diff --git a/.fpm b/.fpm index 546ce641cf..a03eba34a3 100644 --- a/.fpm +++ b/.fpm @@ -1,5 +1,6 @@ -s dir --name defguard ---description "defguard core service" +--description "Defguard Core service" --url "https://defguard.net/" ---maintainer "teonite" +--maintainer "Defguard" +--config-files /etc/defguard/core.conf diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00e0f4f7f1..1939c92b14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,7 +64,6 @@ jobs: build-binaries: needs: [create-release] - runs-on: - self-hosted - Linux @@ -108,7 +107,7 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: 1.89.0 + toolchain: 1.89.0 # "stable" causes rust-lld: error on aarch64-linux target: ${{ matrix.target }} override: true @@ -173,26 +172,6 @@ jobs: fpm_args: "defguard-${{ github.ref_name }}-${{ matrix.target }}=/usr/bin/defguard defguard.service=/usr/lib/systemd/system/defguard.service .env-template=/etc/defguard/core.conf" fpm_opts: "--architecture ${{ matrix.arch }} --debug --output-type deb --version ${{ env.VERSION }} --package defguard-${{ env.VERSION }}-${{ matrix.target }}.deb" - - name: Run `packer init` - if: matrix.build == 'linux' && matrix.arch == 'amd64' - id: init - run: "packer init ./images/ami/core.pkr.hcl" - - - name: Build AMI images for multiple regions - if: matrix.build == 'linux' && matrix.arch == 'amd64' - run: | - regions=(us-east-1 eu-west-1 ap-northeast-1 eu-central-1) - for region in "${regions[@]}"; do - echo "Building AMI for region: $region" - echo "Running packer validate for $region..." - packer validate --var "package_version=${{ env.VERSION }}" --var "region=$region" ./images/ami/core.pkr.hcl - echo "Building AMI image for $region..." - packer build -color=false -on-error=abort --var "package_version=${{ env.VERSION }}" --var "region=$region" ./images/ami/core.pkr.hcl - done - env: - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - - name: Upload DEB if: matrix.build == 'linux' uses: actions/upload-release-asset@v1.0.2 @@ -204,6 +183,20 @@ jobs: asset_name: defguard-${{ env.VERSION }}-${{ matrix.target }}.deb asset_content_type: application/octet-stream + - name: Install ruby with deb-s3 + if: matrix.build == 'linux' + run: | + sudo apt-get install -y ruby + gem install deb-s3 + echo "$(ruby -r rubygems -e 'puts Gem.user_dir')/bin" >> $GITHUB_PATH + + - name: Upload DEB to apt repository + if: matrix.build == 'linux' + run: | + COMPONENT=$([[ "${{ github.ref_name }}" == *"-"* ]] && echo "pre-release" || echo "release") # if tag contain "-" assume it's pre-release. + + deb-s3 upload -l --bucket=apt.defguard.net --access-key-id=${{ secrets.AWS_ACCESS_KEY_APT }} --secret-access-key=${{ secrets.AWS_SECRET_KEY_APT }} --s3-region=eu-north-1 --no-fail-if-exists --codename=trixie --component="$COMPONENT" defguard-${{ env.VERSION }}-${{ matrix.target }}.deb + - name: Build RPM package if: matrix.build == 'linux' uses: defGuard/fpm-action@main @@ -227,7 +220,7 @@ jobs: uses: defGuard/fpm-action@main with: fpm_args: "defguard-${{ github.ref_name }}-${{ matrix.target }}=/usr/local/bin/defguard defguard.service.freebsd=/usr/local/etc/rc.d/defguard" - fpm_opts: "--architecture ${{ matrix.arch }} --debug --output-type freebsd --version ${{ env.VERSION }} --package defguard-${{ env.VERSION }}_${{ matrix.target }}.pkg --freebsd-osversion '*'" + fpm_opts: "--architecture ${{ matrix.arch }} --debug --output-type freebsd --version ${{ env.VERSION }} --package defguard-${{ env.VERSION }}_${{ matrix.target }}.pkg --freebsd-osversion '*' --depends openssl" - name: Upload FreeBSD if: matrix.build == 'freebsd' @@ -239,3 +232,37 @@ jobs: asset_path: defguard-${{ env.VERSION }}_${{ matrix.target }}.pkg asset_name: defguard-${{ env.VERSION }}_${{ matrix.target }}.pkg asset_content_type: application/octet-stream + + apt-sign: + needs: + - build-binaries + runs-on: + - self-hosted + - Linux + - X64 + steps: + - name: Sign APT repository + run: | + export AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_APT }} + export AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_KEY_APT }} + export AWS_REGION=eu-north-1 + sudo apt update -y + sudo apt install -y awscli curl jq + + for DIST in trixie; do + aws s3 cp s3://apt.defguard.net/dists/${DIST}/Release . + + curl -X POST "${{ secrets.DEFGUARD_SIGNING_URL }}?signature_type=both" \ + -H "Authorization: Bearer ${{ secrets.DEFGUARD_SIGNING_API_KEY }}" \ + -F "file=@Release" \ + -o response.json + + cat response.json | jq -r '.files["Release.gpg"].content' | base64 --decode > Release.gpg + cat response.json | jq -r '.files.Release.content' | base64 --decode > InRelease + + aws s3 cp Release.gpg s3://apt.defguard.net/dists/${DIST}/ --acl public-read + aws s3 cp InRelease s3://apt.defguard.net/dists/${DIST}/ --acl public-read + + done + (aws s3 ls s3://apt.defguard.net/dists/ --recursive; aws s3 ls s3://apt.defguard.net/pool/ --recursive) | awk '{print ""$4"
"}' > index.html + aws s3 cp index.html s3://apt.defguard.net/ --acl public-read diff --git a/.github/workflows/test-web.yml b/.github/workflows/test-web.yml new file mode 100644 index 0000000000..2c1b813278 --- /dev/null +++ b/.github/workflows/test-web.yml @@ -0,0 +1,39 @@ +on: + push: + branches: + - main + - dev + - "release/**" + paths-ignore: + - "*.md" + - "LICENSE" + pull_request: + branches: + - main + - dev + - "release/**" + paths-ignore: + - "*.md" + - "LICENSE" + +permissions: + contents: read +jobs: + test-web: + runs-on: + - codebuild-defguard-core-runner-${{ github.run_id }}-${{ github.run_attempt }} + steps: + - uses: actions/checkout@v4 + with: + submodules: "recursive" + - uses: actions/setup-node@v4 + with: + node-version: 24 + - name: install deps + working-directory: ./web + run: | + npm i -g npm pnpm + pnpm i --frozen-lockfile + - name: Run tests + working-directory: ./web + run: pnpm run test diff --git a/.sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json b/.sqlx/query-03710e2a3e96096e57f95a62543174a42e3447f9d2a949b6820e685ddab6561d.json similarity index 77% rename from .sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json rename to .sqlx/query-03710e2a3e96096e57f95a62543174a42e3447f9d2a949b6820e685ddab6561d.json index 36d70341c3..9bb03daf9c 100644 --- a/.sqlx/query-5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b.json +++ b/.sqlx/query-03710e2a3e96096e57f95a62543174a42e3447f9d2a949b6820e685ddab6561d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1", + "query": "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM aclrulenetwork r JOIN wireguard_network n ON n.id = r.network_id WHERE r.rule_id = $1", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -110,8 +126,9 @@ false, false, false, + false, false ] }, - "hash": "5350e57595e044cea6976a73910210e5106af580e45647ae620850de0b77785b" + "hash": "03710e2a3e96096e57f95a62543174a42e3447f9d2a949b6820e685ddab6561d" } diff --git a/.sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json b/.sqlx/query-0c237b235f0455c5f79f2ea4e8210b1ea9e8149f3bb760d2039b463ef262eb03.json similarity index 76% rename from .sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json rename to .sqlx/query-0c237b235f0455c5f79f2ea4e8210b1ea9e8149f3bb760d2039b463ef262eb03.json index 4d94977db3..3b4c356fed 100644 --- a/.sqlx/query-0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48.json +++ b/.sqlx/query-0c237b235f0455c5f79f2ea4e8210b1ea9e8149f3bb760d2039b463ef262eb03.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE id IN (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -110,8 +126,9 @@ false, false, false, + false, false ] }, - "hash": "0fb053b3b00a1fe78f764d2d1d90375d5674fd59fe3018af120ae2ef5fd10f48" + "hash": "0c237b235f0455c5f79f2ea4e8210b1ea9e8149f3bb760d2039b463ef262eb03" } diff --git a/.sqlx/query-08dfc889eda4765110276bfa2f660c2db2a35045a66fff8d25aa3e5763733529.json b/.sqlx/query-11c5c9eaade29091b93d5d3e2fca6bea01b09ec20e7ff70e626488191509bcf4.json similarity index 87% rename from .sqlx/query-08dfc889eda4765110276bfa2f660c2db2a35045a66fff8d25aa3e5763733529.json rename to .sqlx/query-11c5c9eaade29091b93d5d3e2fca6bea01b09ec20e7ff70e626488191509bcf4.json index cfdbec3ef1..c96dd9aff6 100644 --- a/.sqlx/query-08dfc889eda4765110276bfa2f660c2db2a35045a66fff8d25aa3e5763733529.json +++ b/.sqlx/query-11c5c9eaade29091b93d5d3e2fca6bea01b09ec20e7ff70e626488191509bcf4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" u WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id WHERE is_admin = true AND user_id = u.id) AND u.is_active = true", + "query": "\n SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id WHERE is_admin = true AND user_id = u.id) AND u.is_active = true", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -139,8 +144,9 @@ false, false, true, - true + true, + false ] }, - "hash": "08dfc889eda4765110276bfa2f660c2db2a35045a66fff8d25aa3e5763733529" + "hash": "11c5c9eaade29091b93d5d3e2fca6bea01b09ec20e7ff70e626488191509bcf4" } diff --git a/.sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json b/.sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json similarity index 91% rename from .sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json rename to .sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json index f847621f43..8ba998d2cb 100644 --- a/.sqlx/query-9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a.json +++ b/.sqlx/query-14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\" FROM \"openidprovider\" WHERE id = $1", + "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\",\"prefetch_users\" FROM \"openidprovider\" WHERE id = $1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -150,8 +155,9 @@ true, true, false, - true + true, + false ] }, - "hash": "9f98a138560451105b104fc7a4d3d29e22e58f33e902c06bbf6163ee48ae802a" + "hash": "14302b1c6c7d72d6e6f38c80538040b9fa3479c919f1ace2a787470690be9de3" } diff --git a/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json b/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json new file mode 100644 index 0000000000..3b31770567 --- /dev/null +++ b/.sqlx/query-160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8.json @@ -0,0 +1,43 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT admin_device_management, client_traffic_policy \"client_traffic_policy: ClientTrafficPolicy\", only_client_activation FROM \"enterprisesettings\" WHERE id = 1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "admin_device_management", + "type_info": "Bool" + }, + { + "ordinal": 1, + "name": "client_traffic_policy: ClientTrafficPolicy", + "type_info": { + "Custom": { + "name": "client_traffic_policy", + "kind": { + "Enum": [ + "none", + "disable_all_traffic", + "force_all_traffic" + ] + } + } + } + }, + { + "ordinal": 2, + "name": "only_client_activation", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "160d23b882d0465fbc8c5453b7dba68521649ba86985d45049487ae50d7dfde8" +} diff --git a/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json b/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json deleted file mode 100644 index c1696c5b03..0000000000 --- a/.sqlx/query-283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT admin_device_management, disable_all_traffic, only_client_activation FROM \"enterprisesettings\" WHERE id = 1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "admin_device_management", - "type_info": "Bool" - }, - { - "ordinal": 1, - "name": "disable_all_traffic", - "type_info": "Bool" - }, - { - "ordinal": 2, - "name": "only_client_activation", - "type_info": "Bool" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false - ] - }, - "hash": "283e1c3d082f1388fc2b806bdcab715db1c1df67da573b0f132fea265e42b416" -} diff --git a/.sqlx/query-9aa803c71e997fb57e12108041077b49a6b3fbf202c63031bf38e9c68cc1fb31.json b/.sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json similarity index 89% rename from .sqlx/query-9aa803c71e997fb57e12108041077b49a6b3fbf202c63031bf38e9c68cc1fb31.json rename to .sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json index 4294f4f023..0de478df57 100644 --- a/.sqlx/query-9aa803c71e997fb57e12108041077b49a6b3fbf202c63031bf38e9c68cc1fb31.json +++ b/.sqlx/query-37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE ldap_user_path IS NULL\n ", + "query": "\n SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE ldap_user_path IS NULL\n ", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -139,8 +144,9 @@ false, false, true, - true + true, + false ] }, - "hash": "9aa803c71e997fb57e12108041077b49a6b3fbf202c63031bf38e9c68cc1fb31" + "hash": "37214c3279207b0d6e33b46726f35ed13ec41c1209e82412c4f7a70d39b21aca" } diff --git a/.sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json b/.sqlx/query-38970835606c0fb0d2cf1d87253cc2deb6e638c229a1929afe35d79dddfbdf67.json similarity index 77% rename from .sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json rename to .sqlx/query-38970835606c0fb0d2cf1d87253cc2deb6e638c229a1929afe35d79dddfbdf67.json index cee5190d9a..3fbc28224b 100644 --- a/.sqlx/query-d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e.json +++ b/.sqlx/query-38970835606c0fb0d2cf1d87253cc2deb6e638c229a1929afe35d79dddfbdf67.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE location_mfa_mode != 'disabled'::location_mfa_mode", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE location_mfa_mode != 'disabled'::location_mfa_mode", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -108,8 +124,9 @@ false, false, false, + false, false ] }, - "hash": "d6298964d89e9fdb087f7860a38f2f325d5ec81e6e0ea10660f74084718ac48e" + "hash": "38970835606c0fb0d2cf1d87253cc2deb6e638c229a1929afe35d79dddfbdf67" } diff --git a/.sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json b/.sqlx/query-4628fb49dfe7cb4e9470018f566342f9e63cf2ebf64a517adeea511ba932e2a0.json similarity index 78% rename from .sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json rename to .sqlx/query-4628fb49dfe7cb4e9470018f566342f9e63cf2ebf64a517adeea511ba932e2a0.json index 1b397c0991..75b2c85a66 100644 --- a/.sqlx/query-21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48.json +++ b/.sqlx/query-4628fb49dfe7cb4e9470018f566342f9e63cf2ebf64a517adeea511ba932e2a0.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE id = $1", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE id = $1", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -110,8 +126,9 @@ false, false, false, + false, false ] }, - "hash": "21957027aa29a30a186e87441b86eadc2d27eeb56d18f9debf50d0ba71e01e48" + "hash": "4628fb49dfe7cb4e9470018f566342f9e63cf2ebf64a517adeea511ba932e2a0" } diff --git a/.sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json b/.sqlx/query-4ad6e8059b230eee547c234d02f2d0aea3e87da3082b49d7f6d6dd82630597ac.json similarity index 77% rename from .sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json rename to .sqlx/query-4ad6e8059b230eee547c234d02f2d0aea3e87da3082b49d7f6d6dd82630597ac.json index 55255fc63f..613c96a482 100644 --- a/.sqlx/query-89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575.json +++ b/.sqlx/query-4ad6e8059b230eee547c234d02f2d0aea3e87da3082b49d7f6d6dd82630597ac.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE location_mfa_mode = 'external'::location_mfa_mode", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE location_mfa_mode = 'external'::location_mfa_mode", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -108,8 +124,9 @@ false, false, false, + false, false ] }, - "hash": "89ec538b614f4ea6440ce15ec58044149463f9a39f673b1f83587980c527c575" + "hash": "4ad6e8059b230eee547c234d02f2d0aea3e87da3082b49d7f6d6dd82630597ac" } diff --git a/.sqlx/query-9387577dcb8f6d64052ddc8b28ef6b60bd37fd6d6a6b71fad7103260f8479e8b.json b/.sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json similarity index 90% rename from .sqlx/query-9387577dcb8f6d64052ddc8b28ef6b60bd37fd6d6a6b71fad7103260f8479e8b.json rename to .sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json index 87955ba2a5..872ad26651 100644 --- a/.sqlx/query-9387577dcb8f6d64052ddc8b28ef6b60bd37fd6d6a6b71fad7103260f8479e8b.json +++ b/.sqlx/query-4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE username = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE openid_sub = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "9387577dcb8f6d64052ddc8b28ef6b60bd37fd6d6a6b71fad7103260f8479e8b" + "hash": "4b7289fe59a0e9553fa0bfd9e9da2dbd2cda17952cf77ac3d743a4fa85d37482" } diff --git a/.sqlx/query-865dc85f17a0fef547a43c402719b86252990422a10231a53313e9b9af249a11.json b/.sqlx/query-4d28fb2ad919eafeefd19e2796ce6db89b7e0afa4f7301eb6e2c9fefe604042d.json similarity index 90% rename from .sqlx/query-865dc85f17a0fef547a43c402719b86252990422a10231a53313e9b9af249a11.json rename to .sqlx/query-4d28fb2ad919eafeefd19e2796ce6db89b7e0afa4f7301eb6e2c9fefe604042d.json index 2f56a55c4c..790bbc027c 100644 --- a/.sqlx/query-865dc85f17a0fef547a43c402719b86252990422a10231a53313e9b9af249a11.json +++ b/.sqlx/query-4d28fb2ad919eafeefd19e2796ce6db89b7e0afa4f7301eb6e2c9fefe604042d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE is_active = true", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE is_active = true", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -139,8 +144,9 @@ false, false, true, - true + true, + false ] }, - "hash": "865dc85f17a0fef547a43c402719b86252990422a10231a53313e9b9af249a11" + "hash": "4d28fb2ad919eafeefd19e2796ce6db89b7e0afa4f7301eb6e2c9fefe604042d" } diff --git a/.sqlx/query-f2f3c5edc14b5b46a34fafdfb71e5cff07adc5b237228d8337004b803ac932b5.json b/.sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json similarity index 81% rename from .sqlx/query-f2f3c5edc14b5b46a34fafdfb71e5cff07adc5b237228d8337004b803ac932b5.json rename to .sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json index fda9187be2..cf84d03729 100644 --- a/.sqlx/query-f2f3c5edc14b5b46a34fafdfb71e5cff07adc5b237228d8337004b803ac932b5.json +++ b/.sqlx/query-523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) RETURNING id", + "query": "INSERT INTO \"user\" (\"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\",\"recovery_codes\",\"enrollment_pending\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20) RETURNING id", "describe": { "columns": [ { @@ -41,12 +41,13 @@ } } }, - "TextArray" + "TextArray", + "Bool" ] }, "nullable": [ false ] }, - "hash": "f2f3c5edc14b5b46a34fafdfb71e5cff07adc5b237228d8337004b803ac932b5" + "hash": "523933d20a62a730c3eb881814200ed1b03ab9dbe8de5368321466b0256d028c" } diff --git a/.sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json b/.sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json similarity index 92% rename from .sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json rename to .sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json index dec553bccb..bfd59988cf 100644 --- a/.sqlx/query-06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77.json +++ b/.sqlx/query-558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled,\n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key FROM openidprovider WHERE name = $1", + "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled,\n directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users FROM openidprovider WHERE name = $1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -150,8 +155,9 @@ true, true, false, - true + true, + false ] }, - "hash": "06bbd4a7662ea9ec62a0138efa9acb62c4bd9b646846740333d8ae3d154d1d77" + "hash": "558fb8aa5e223f6fc273c1431410dabb2ca9c2a831cacb7ebc8d696020b0556c" } diff --git a/.sqlx/query-f9b514b027dbca84d45db0a1d4cb3f58927885992773f753abf0a524ff2d783f.json b/.sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json similarity index 88% rename from .sqlx/query-f9b514b027dbca84d45db0a1d4cb3f58927885992773f753abf0a524ff2d783f.json rename to .sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json index 3334968c24..48a9989af2 100644 --- a/.sqlx/query-f9b514b027dbca84d45db0a1d4cb3f58927885992773f753abf0a524ff2d783f.json +++ b/.sqlx/query-5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", + "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" JOIN group_user ON \"user\".id = group_user.user_id WHERE group_user.group_id = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "f9b514b027dbca84d45db0a1d4cb3f58927885992773f753abf0a524ff2d783f" + "hash": "5b9e2982325e824daab097a76e29c23d7e5d4ebac61f87d98c441a12e14e8774" } diff --git a/.sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json b/.sqlx/query-60f62225c964c16171bfe23148757e657dcb71c90e209102b4033888f5c668cd.json similarity index 78% rename from .sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json rename to .sqlx/query-60f62225c964c16171bfe23148757e657dcb71c90e209102b4033888f5c668cd.json index 4b43522b73..e221cd0a9d 100644 --- a/.sqlx/query-6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc.json +++ b/.sqlx/query-60f62225c964c16171bfe23148757e657dcb71c90e209102b4033888f5c668cd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" FROM wireguard_network WHERE name = $1", + "query": "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", service_location_mode \"service_location_mode: ServiceLocationMode\" FROM wireguard_network WHERE name = $1", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: ServiceLocationMode", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -110,8 +126,9 @@ false, false, false, + false, false ] }, - "hash": "6f207a4d39d616b6cfdb10e4e4e7e2bf4a03081f33c0b4e779e5e431b092fbdc" + "hash": "60f62225c964c16171bfe23148757e657dcb71c90e209102b4033888f5c668cd" } diff --git a/.sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json b/.sqlx/query-7938837afdae9daee7a22d0ed919c33c6e14d74fb23bd9baf450dc9827d547c6.json similarity index 80% rename from .sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json rename to .sqlx/query-7938837afdae9daee7a22d0ed919c33c6e14d74fb23bd9baf450dc9827d547c6.json index da1e780cd7..8a209a1cf5 100644 --- a/.sqlx/query-0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab.json +++ b/.sqlx/query-7938837afdae9daee7a22d0ed919c33c6e14d74fb23bd9baf450dc9827d547c6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\" FROM \"wireguard_network\"", + "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\",\"service_location_mode\" \"service_location_mode: _\" FROM \"wireguard_network\"", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: _", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -108,8 +124,9 @@ false, false, false, + false, false ] }, - "hash": "0c7b5094f1e4dc79a5782b6948aa2527807bd9643f2758c83c728d1d003843ab" + "hash": "7938837afdae9daee7a22d0ed919c33c6e14d74fb23bd9baf450dc9827d547c6" } diff --git a/.sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json b/.sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json similarity index 90% rename from .sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json rename to .sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json index e28198163d..528c633238 100644 --- a/.sqlx/query-d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb.json +++ b/.sqlx/query-796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4, display_name = $5, google_service_account_key = $6, google_service_account_email = $7, admin_email = $8, directory_sync_enabled = $9, directory_sync_interval = $10, directory_sync_user_behavior = $11, directory_sync_admin_behavior = $12, directory_sync_target = $13, okta_private_jwk = $14, okta_dirsync_client_id = $15, directory_sync_group_match = $16, jumpcloud_api_key = $17 WHERE id = $18", + "query": "UPDATE openidprovider SET name = $1, base_url = $2, client_id = $3, client_secret = $4, display_name = $5, google_service_account_key = $6, google_service_account_email = $7, admin_email = $8, directory_sync_enabled = $9, directory_sync_interval = $10, directory_sync_user_behavior = $11, directory_sync_admin_behavior = $12, directory_sync_target = $13, okta_private_jwk = $14, okta_dirsync_client_id = $15, directory_sync_group_match = $16, jumpcloud_api_key = $17, prefetch_users = $18 WHERE id = $19", "describe": { "columns": [], "parameters": { @@ -55,10 +55,11 @@ "Text", "TextArray", "Text", + "Bool", "Int8" ] }, "nullable": [] }, - "hash": "d4d76206a3eeb48f4c3e06e53e781bab2a0e2020e33653ef34ab1ea7df67a0cb" + "hash": "796ef2b0b73f5689a592497320b98ddb54a2a673a531cb882aadea3d5aa25d66" } diff --git a/.sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json b/.sqlx/query-7cb2f4e82c2d2cb1aec62bc3ae335034418d6224e190ce6ae96b8779e2505062.json similarity index 68% rename from .sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json rename to .sqlx/query-7cb2f4e82c2d2cb1aec62bc3ae335034418d6224e190ce6ae96b8779e2505062.json index 070daed9c5..6315118ca6 100644 --- a/.sqlx/query-966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a.json +++ b/.sqlx/query-7cb2f4e82c2d2cb1aec62bc3ae335034418d6224e190ce6ae96b8779e2505062.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id", + "query": "INSERT INTO \"wireguard_network\" (\"name\",\"address\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\",\"service_location_mode\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) RETURNING id", "describe": { "columns": [ { @@ -35,6 +35,18 @@ ] } } + }, + { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } } ] }, @@ -42,5 +54,5 @@ false ] }, - "hash": "966dd7a3677babebc8e34b6f502e7e1aea7304e054c1241406aa62993d66117a" + "hash": "7cb2f4e82c2d2cb1aec62bc3ae335034418d6224e190ce6ae96b8779e2505062" } diff --git a/.sqlx/query-88cead8923416e5c3b6689470daff40dc34b54678899d3bfc9edcefe16fcf43c.json b/.sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json similarity index 90% rename from .sqlx/query-88cead8923416e5c3b6689470daff40dc34b54678899d3bfc9edcefe16fcf43c.json rename to .sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json index 59dfb194d2..95c30504ab 100644 --- a/.sqlx/query-88cead8923416e5c3b6689470daff40dc34b54678899d3bfc9edcefe16fcf43c.json +++ b/.sqlx/query-84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\" WHERE id = $1", + "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "recovery_codes: _", "type_info": "TextArray" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ true, true, false, + false, false ] }, - "hash": "88cead8923416e5c3b6689470daff40dc34b54678899d3bfc9edcefe16fcf43c" + "hash": "84b5185c8c297717e9935cd2521bbbb2ed2bf453b32cb1c22b227d8541a63392" } diff --git a/.sqlx/query-6de98bad88b7a3b01ffb107637f053b4ca587bb59340cfdd463fd04dbd7f8991.json b/.sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json similarity index 86% rename from .sqlx/query-6de98bad88b7a3b01ffb107637f053b4ca587bb59340cfdd463fd04dbd7f8991.json rename to .sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json index b8bfc1f022..3ba7d6e933 100644 --- a/.sqlx/query-6de98bad88b7a3b01ffb107637f053b4ca587bb59340cfdd463fd04dbd7f8991.json +++ b/.sqlx/query-98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id WHERE \"group\".name = $1", + "query": "SELECT \"user\".id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id WHERE \"group\".name = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "6de98bad88b7a3b01ffb107637f053b4ca587bb59340cfdd463fd04dbd7f8991" + "hash": "98d0dc6568f54ebee7efaf5ee6965afbee229e80bbaecc1b321c9e309c70409c" } diff --git a/.sqlx/query-f49077bed4fb50e783b98e5ce0e9ad592f72285ee9165c5d2564a4610608b358.json b/.sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json similarity index 90% rename from .sqlx/query-f49077bed4fb50e783b98e5ce0e9ad592f72285ee9165c5d2564a4610608b358.json rename to .sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json index 918146a1d4..bb0ffd7dcf 100644 --- a/.sqlx/query-f49077bed4fb50e783b98e5ce0e9ad592f72285ee9165c5d2564a4610608b358.json +++ b/.sqlx/query-9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE id = ANY($1)", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE id = ANY($1)", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "f49077bed4fb50e783b98e5ce0e9ad592f72285ee9165c5d2564a4610608b358" + "hash": "9a96f2f262ba86ab421f5f3b08984707951915fdac31ecc117dfbdbd0434366d" } diff --git a/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json b/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json new file mode 100644 index 0000000000..509af4943e --- /dev/null +++ b/.sqlx/query-a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25.json @@ -0,0 +1,27 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE \"enterprisesettings\" SET admin_device_management = $1, client_traffic_policy = $2, only_client_activation = $3 WHERE id = 1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Bool", + { + "Custom": { + "name": "client_traffic_policy", + "kind": { + "Enum": [ + "none", + "disable_all_traffic", + "force_all_traffic" + ] + } + } + }, + "Bool" + ] + }, + "nullable": [] + }, + "hash": "a644507ebcfb9ef04883ad8b07bb3dbb0fc747ae0843baa001e47ea328e49c25" +} diff --git a/.sqlx/query-9104f25ba81fa2589759e9d3ad6a932e984557fb7bc7807bb1c5a3784eaecd3a.json b/.sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json similarity index 90% rename from .sqlx/query-9104f25ba81fa2589759e9d3ad6a932e984557fb7bc7807bb1c5a3784eaecd3a.json rename to .sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json index 98b4e67056..0b95519c07 100644 --- a/.sqlx/query-9104f25ba81fa2589759e9d3ad6a932e984557fb7bc7807bb1c5a3784eaecd3a.json +++ b/.sqlx/query-afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" u JOIN \"device\" d ON u.id = d.user_id WHERE d.id = $1", + "query": "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u JOIN \"device\" d ON u.id = d.user_id WHERE d.id = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "9104f25ba81fa2589759e9d3ad6a932e984557fb7bc7807bb1c5a3784eaecd3a" + "hash": "afbebcf9d6784d63227376d154a35a002501d911b81f03ae5f969e69a9a4ffa5" } diff --git a/.sqlx/query-e03f1fbd03f348b0ec4a4411df8dce8a890be3fd09ceff9a26a4ce661733b88e.json b/.sqlx/query-b5598af6a2b352c73c4bdf5b93f21042ab6b05212938918eebe43cd56795ac76.json similarity index 88% rename from .sqlx/query-e03f1fbd03f348b0ec4a4411df8dce8a890be3fd09ceff9a26a4ce661733b88e.json rename to .sqlx/query-b5598af6a2b352c73c4bdf5b93f21042ab6b05212938918eebe43cd56795ac76.json index 1f0b827afd..88a86ecca5 100644 --- a/.sqlx/query-e03f1fbd03f348b0ec4a4411df8dce8a890be3fd09ceff9a26a4ce661733b88e.json +++ b/.sqlx/query-b5598af6a2b352c73c4bdf5b93f21042ab6b05212938918eebe43cd56795ac76.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND NOT r.allow AND u.is_active = true", + "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND NOT r.allow AND u.is_active = true", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "e03f1fbd03f348b0ec4a4411df8dce8a890be3fd09ceff9a26a4ce661733b88e" + "hash": "b5598af6a2b352c73c4bdf5b93f21042ab6b05212938918eebe43cd56795ac76" } diff --git a/.sqlx/query-e428a1588ae20f1b217396600a4181dd9567568ff05c5ebcd94677aa66010c6c.json b/.sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json similarity index 83% rename from .sqlx/query-e428a1588ae20f1b217396600a4181dd9567568ff05c5ebcd94677aa66010c6c.json rename to .sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json index 90c559cc22..22086c1e09 100644 --- a/.sqlx/query-e428a1588ae20f1b217396600a4181dd9567568ff05c5ebcd94677aa66010c6c.json +++ b/.sqlx/query-b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"from_ldap\" = $10,\"ldap_pass_randomized\" = $11,\"ldap_rdn\" = $12,\"ldap_user_path\" = $13,\"openid_sub\" = $14,\"totp_enabled\" = $15,\"email_mfa_enabled\" = $16,\"totp_secret\" = $17,\"email_mfa_secret\" = $18,\"mfa_method\" = $19,\"recovery_codes\" = $20 WHERE id = $1", + "query": "UPDATE \"user\" SET \"username\" = $2,\"password_hash\" = $3,\"last_name\" = $4,\"first_name\" = $5,\"email\" = $6,\"phone\" = $7,\"mfa_enabled\" = $8,\"is_active\" = $9,\"from_ldap\" = $10,\"ldap_pass_randomized\" = $11,\"ldap_rdn\" = $12,\"ldap_user_path\" = $13,\"openid_sub\" = $14,\"totp_enabled\" = $15,\"email_mfa_enabled\" = $16,\"totp_secret\" = $17,\"email_mfa_secret\" = $18,\"mfa_method\" = $19,\"recovery_codes\" = $20,\"enrollment_pending\" = $21 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -36,10 +36,11 @@ } } }, - "TextArray" + "TextArray", + "Bool" ] }, "nullable": [] }, - "hash": "e428a1588ae20f1b217396600a4181dd9567568ff05c5ebcd94677aa66010c6c" + "hash": "b855a2bc8e31ac52f8e23f783652116227f55516d17150bd4f370d21f2e1a46b" } diff --git a/.sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json b/.sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json similarity index 85% rename from .sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json rename to .sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json index 8902fca66f..7994ebf797 100644 --- a/.sqlx/query-dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff.json +++ b/.sqlx/query-c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"openidprovider\" (\"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\",\"directory_sync_admin_behavior\",\"directory_sync_target\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\",\"jumpcloud_api_key\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) RETURNING id", + "query": "INSERT INTO \"openidprovider\" (\"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\",\"directory_sync_admin_behavior\",\"directory_sync_target\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\",\"jumpcloud_api_key\",\"prefetch_users\") VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18) RETURNING id", "describe": { "columns": [ { @@ -60,12 +60,13 @@ "Text", "Text", "TextArray", - "Text" + "Text", + "Bool" ] }, "nullable": [ false ] }, - "hash": "dce467a600d7b0e51d1b75dd5978c56cc1e6b0c6fbf1907cce4bbe0a1bde88ff" + "hash": "c6ebb402f91d242754872addc3604fb6ffac7e6fdd0d9428070cecb68c666cd8" } diff --git a/.sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json b/.sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json similarity index 90% rename from .sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json rename to .sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json index 3576fdb99b..5c80f34900 100644 --- a/.sqlx/query-187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483.json +++ b/.sqlx/query-c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"openidprovider\" SET \"name\" = $2,\"base_url\" = $3,\"client_id\" = $4,\"client_secret\" = $5,\"display_name\" = $6,\"google_service_account_key\" = $7,\"google_service_account_email\" = $8,\"admin_email\" = $9,\"directory_sync_enabled\" = $10,\"directory_sync_interval\" = $11,\"directory_sync_user_behavior\" = $12,\"directory_sync_admin_behavior\" = $13,\"directory_sync_target\" = $14,\"okta_private_jwk\" = $15,\"okta_dirsync_client_id\" = $16,\"directory_sync_group_match\" = $17,\"jumpcloud_api_key\" = $18 WHERE id = $1", + "query": "UPDATE \"openidprovider\" SET \"name\" = $2,\"base_url\" = $3,\"client_id\" = $4,\"client_secret\" = $5,\"display_name\" = $6,\"google_service_account_key\" = $7,\"google_service_account_email\" = $8,\"admin_email\" = $9,\"directory_sync_enabled\" = $10,\"directory_sync_interval\" = $11,\"directory_sync_user_behavior\" = $12,\"directory_sync_admin_behavior\" = $13,\"directory_sync_target\" = $14,\"okta_private_jwk\" = $15,\"okta_dirsync_client_id\" = $16,\"directory_sync_group_match\" = $17,\"jumpcloud_api_key\" = $18,\"prefetch_users\" = $19 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -55,10 +55,11 @@ "Text", "Text", "TextArray", - "Text" + "Text", + "Bool" ] }, "nullable": [] }, - "hash": "187b82f0cc866ff2f1049aa57d9477cbad81d77c2db2b67dca90de198721b483" + "hash": "c8e9800861c7bc853235858650be8ad3d3d19b0c4f0e69b9002a6a1fbd46a324" } diff --git a/.sqlx/query-0e1afbbf6d2f59c7ff6c3b6e079c262162fab493a5c394730f10cfc48c9c91a0.json b/.sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json similarity index 90% rename from .sqlx/query-0e1afbbf6d2f59c7ff6c3b6e079c262162fab493a5c394730f10cfc48c9c91a0.json rename to .sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json index 38d6a590b6..6e0608e1d0 100644 --- a/.sqlx/query-0e1afbbf6d2f59c7ff6c3b6e079c262162fab493a5c394730f10cfc48c9c91a0.json +++ b/.sqlx/query-c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE email ILIKE $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE email ILIKE $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "0e1afbbf6d2f59c7ff6c3b6e079c262162fab493a5c394730f10cfc48c9c91a0" + "hash": "c973c8ca54523b237b7b3b4c95d9603a13056d395734ba2399cd11c146f4d473" } diff --git a/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json b/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json deleted file mode 100644 index 787a75d7d9..0000000000 --- a/.sqlx/query-ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "UPDATE \"enterprisesettings\" SET admin_device_management = $1, disable_all_traffic = $2, only_client_activation = $3 WHERE id = 1", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Bool", - "Bool", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "ccd62ea7526078c9db47812e7f6a5e7829eae217ad9f7f3b0b03aa02f8808dc2" -} diff --git a/.sqlx/query-1e08b3fc959f39560270027723ee12284a2532464a8f59d37699dcff97c3ba63.json b/.sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json similarity index 90% rename from .sqlx/query-1e08b3fc959f39560270027723ee12284a2532464a8f59d37699dcff97c3ba63.json rename to .sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json index 0d1ffd478e..c2515c8b66 100644 --- a/.sqlx/query-1e08b3fc959f39560270027723ee12284a2532464a8f59d37699dcff97c3ba63.json +++ b/.sqlx/query-d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE id = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE id = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "1e08b3fc959f39560270027723ee12284a2532464a8f59d37699dcff97c3ba63" + "hash": "d1595e6cfd8947354669341ac26ac5d84efa6746a9cff3e28db6d8be28e7f69c" } diff --git a/.sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json b/.sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json similarity index 92% rename from .sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json rename to .sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json index 5a629b4c9f..29d36e4226 100644 --- a/.sqlx/query-07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee.json +++ b/.sqlx/query-d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\" FROM \"openidprovider\"", + "query": "SELECT id, \"name\",\"base_url\",\"client_id\",\"client_secret\",\"display_name\",\"google_service_account_key\",\"google_service_account_email\",\"admin_email\",\"directory_sync_enabled\",\"directory_sync_interval\",\"directory_sync_user_behavior\" \"directory_sync_user_behavior: _\",\"directory_sync_admin_behavior\" \"directory_sync_admin_behavior: _\",\"directory_sync_target\" \"directory_sync_target: _\",\"okta_private_jwk\",\"okta_dirsync_client_id\",\"directory_sync_group_match\" \"directory_sync_group_match: _\",\"jumpcloud_api_key\",\"prefetch_users\" FROM \"openidprovider\"", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -148,8 +153,9 @@ true, true, false, - true + true, + false ] }, - "hash": "07ac05be4850e0154414090784fc40392f423c16cd326716994fcb1f45c84eee" + "hash": "d8db674150231de0063227000a8b39f45c6da9836f93b67307787851a1804f13" } diff --git a/.sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json b/.sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json similarity index 92% rename from .sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json rename to .sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json index 8b48d798c8..f59dbf3807 100644 --- a/.sqlx/query-6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f.json +++ b/.sqlx/query-e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key FROM openidprovider LIMIT 1", + "query": "SELECT id, name, base_url, client_id, client_secret, display_name, google_service_account_key, google_service_account_email, admin_email, directory_sync_enabled, directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", directory_sync_target \"directory_sync_target: DirectorySyncTarget\", okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users FROM openidprovider LIMIT 1", "describe": { "columns": [ { @@ -125,6 +125,11 @@ "ordinal": 17, "name": "jumpcloud_api_key", "type_info": "Text" + }, + { + "ordinal": 18, + "name": "prefetch_users", + "type_info": "Bool" } ], "parameters": { @@ -148,8 +153,9 @@ true, true, false, - true + true, + false ] }, - "hash": "6c3bbaa998dbb9d0b3771c546b014818139cdfac6ed6c15603f6e6806c63ac6f" + "hash": "e28b02ccc616d67fcb1a1aa940ea35be58cb652e8287a6f5028421656048b58a" } diff --git a/.sqlx/query-58f875f68fdee165045a6ab1185d14726f9aa86e613ab050358ca8ea455693e7.json b/.sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json similarity index 88% rename from .sqlx/query-58f875f68fdee165045a6ab1185d14726f9aa86e613ab050358ca8ea455693e7.json rename to .sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json index 25a55dc8c4..aeb15fac8e 100644 --- a/.sqlx/query-58f875f68fdee165045a6ab1185d14726f9aa86e613ab050358ca8ea455693e7.json +++ b/.sqlx/query-e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" u JOIN group_user gu ON u.id=gu.user_id WHERE u.is_active=true AND gu.group_id=ANY($1)", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" u JOIN group_user gu ON u.id=gu.user_id WHERE u.is_active=true AND gu.group_id=ANY($1)", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "58f875f68fdee165045a6ab1185d14726f9aa86e613ab050358ca8ea455693e7" + "hash": "e5a4b8671069997f4ae7fc38c33ffbae9869524f2ecf68ee407ab9ac4fae4cb4" } diff --git a/.sqlx/query-a6dfaf41f375066925c5e15dbdd649eac7e2eb3bbe57be5d9ac0f43964064ed2.json b/.sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json similarity index 91% rename from .sqlx/query-a6dfaf41f375066925c5e15dbdd649eac7e2eb3bbe57be5d9ac0f43964064ed2.json rename to .sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json index a2c50d66a5..01a2303485 100644 --- a/.sqlx/query-a6dfaf41f375066925c5e15dbdd649eac7e2eb3bbe57be5d9ac0f43964064ed2.json +++ b/.sqlx/query-f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\" FROM \"user\"", + "query": "SELECT id, \"username\",\"password_hash\",\"last_name\",\"first_name\",\"email\",\"phone\",\"mfa_enabled\",\"is_active\",\"from_ldap\",\"ldap_pass_randomized\",\"ldap_rdn\",\"ldap_user_path\",\"openid_sub\",\"totp_enabled\",\"email_mfa_enabled\",\"totp_secret\",\"email_mfa_secret\",\"mfa_method\" \"mfa_method: _\",\"recovery_codes\" \"recovery_codes: _\",\"enrollment_pending\" FROM \"user\"", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "recovery_codes: _", "type_info": "TextArray" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -139,8 +144,9 @@ true, true, false, + false, false ] }, - "hash": "a6dfaf41f375066925c5e15dbdd649eac7e2eb3bbe57be5d9ac0f43964064ed2" + "hash": "f3568532db86448be0fcdc2bcdea805d0b5b6a222ed4036da7880c57c9790091" } diff --git a/.sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json b/.sqlx/query-f7c1feed3561417bc20420558c27f7835e7ef3e0312aef9c56e495f05f11b4dd.json similarity index 79% rename from .sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json rename to .sqlx/query-f7c1feed3561417bc20420558c27f7835e7ef3e0312aef9c56e495f05f11b4dd.json index 37b1fe0d05..f238310936 100644 --- a/.sqlx/query-32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c.json +++ b/.sqlx/query-f7c1feed3561417bc20420558c27f7835e7ef3e0312aef9c56e495f05f11b4dd.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\" FROM \"wireguard_network\" WHERE id = $1", + "query": "SELECT id, \"name\",\"address\" \"address: _\",\"port\",\"pubkey\",\"prvkey\",\"endpoint\",\"dns\",\"allowed_ips\" \"allowed_ips: _\",\"connected_at\",\"acl_enabled\",\"acl_default_allow\",\"keepalive_interval\",\"peer_disconnect_threshold\",\"location_mfa_mode\" \"location_mfa_mode: _\",\"service_location_mode\" \"service_location_mode: _\" FROM \"wireguard_network\" WHERE id = $1", "describe": { "columns": [ { @@ -88,6 +88,22 @@ } } } + }, + { + "ordinal": 15, + "name": "service_location_mode: _", + "type_info": { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } + } } ], "parameters": { @@ -110,8 +126,9 @@ false, false, false, + false, false ] }, - "hash": "32187156f93aaff898a4445056a2a332453a9626e56a5f543c2790bff9d2109c" + "hash": "f7c1feed3561417bc20420558c27f7835e7ef3e0312aef9c56e495f05f11b4dd" } diff --git a/.sqlx/query-dc7082efd9915800029f47135458b87498913f37463f974fb6f1935225c37ea8.json b/.sqlx/query-f98428d4f6faeaf5475a9292045ce68fdd5e37d84269497c82184b1bb1db710e.json similarity index 88% rename from .sqlx/query-dc7082efd9915800029f47135458b87498913f37463f974fb6f1935225c37ea8.json rename to .sqlx/query-f98428d4f6faeaf5475a9292045ce68fdd5e37d84269497c82184b1bb1db710e.json index a4302b3f3c..18ccc08762 100644 --- a/.sqlx/query-dc7082efd9915800029f47135458b87498913f37463f974fb6f1935225c37ea8.json +++ b/.sqlx/query-f98428d4f6faeaf5475a9292045ce68fdd5e37d84269497c82184b1bb1db710e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND r.allow AND u.is_active = true", + "query": "SELECT u.id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM aclruleuser r JOIN \"user\" u ON u.id = r.user_id WHERE r.rule_id = $1 AND r.allow AND u.is_active = true", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "dc7082efd9915800029f47135458b87498913f37463f974fb6f1935225c37ea8" + "hash": "f98428d4f6faeaf5475a9292045ce68fdd5e37d84269497c82184b1bb1db710e" } diff --git a/.sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json b/.sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json similarity index 90% rename from .sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json rename to .sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json index 7396561c7f..5280946e04 100644 --- a/.sqlx/query-04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0.json +++ b/.sqlx/query-fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path FROM \"user\" WHERE openid_sub = $1", + "query": "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending FROM \"user\" WHERE username = $1", "describe": { "columns": [ { @@ -114,6 +114,11 @@ "ordinal": 19, "name": "ldap_user_path", "type_info": "Text" + }, + { + "ordinal": 20, + "name": "enrollment_pending", + "type_info": "Bool" } ], "parameters": { @@ -141,8 +146,9 @@ false, false, true, - true + true, + false ] }, - "hash": "04163625da5365779cb5e2a47242b7d7faff6d6327e279362212cda4960caca0" + "hash": "fd671557afabd523045f73a2f32e876821bd29b49c096d798754857b22089c21" } diff --git a/.sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json b/.sqlx/query-fe2ad01a923b3d66c74343dbd9c385908407c4f642d3005e36df113a1aee841d.json similarity index 70% rename from .sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json rename to .sqlx/query-fe2ad01a923b3d66c74343dbd9c385908407c4f642d3005e36df113a1aee841d.json index 933ffa8b91..88bf4542cf 100644 --- a/.sqlx/query-acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0.json +++ b/.sqlx/query-fe2ad01a923b3d66c74343dbd9c385908407c4f642d3005e36df113a1aee841d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"acl_enabled\" = $11,\"acl_default_allow\" = $12,\"keepalive_interval\" = $13,\"peer_disconnect_threshold\" = $14,\"location_mfa_mode\" = $15 WHERE id = $1", + "query": "UPDATE \"wireguard_network\" SET \"name\" = $2,\"address\" = $3,\"port\" = $4,\"pubkey\" = $5,\"prvkey\" = $6,\"endpoint\" = $7,\"dns\" = $8,\"allowed_ips\" = $9,\"connected_at\" = $10,\"acl_enabled\" = $11,\"acl_default_allow\" = $12,\"keepalive_interval\" = $13,\"peer_disconnect_threshold\" = $14,\"location_mfa_mode\" = $15,\"service_location_mode\" = $16 WHERE id = $1", "describe": { "columns": [], "parameters": { @@ -30,10 +30,22 @@ ] } } + }, + { + "Custom": { + "name": "service_location_mode", + "kind": { + "Enum": [ + "disabled", + "prelogon", + "alwayson" + ] + } + } } ] }, "nullable": [] }, - "hash": "acb58694a7dc5ac4268c88e2d37a50697d20952973b676d574b01d0836f1aff0" + "hash": "fe2ad01a923b3d66c74343dbd9c385908407c4f642d3005e36df113a1aee841d" } diff --git a/Cargo.lock b/Cargo.lock index b365037c2c..a0722d94d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -76,9 +67,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -113,9 +104,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -128,9 +119,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -143,22 +134,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -167,6 +158,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -284,9 +284,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "axum" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" dependencies = [ "axum-core", "bytes", @@ -303,8 +303,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -329,9 +328,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -340,7 +339,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -349,9 +347,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" dependencies = [ "axum", "axum-core", @@ -366,27 +364,12 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "serde", + "serde_core", "serde_html_form", "serde_path_to_error", - "tower", "tower-layer", "tower-service", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "tracing", ] [[package]] @@ -427,9 +410,9 @@ checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "base64urlsafedata" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5913e643e4dfb43d5908e9e6f1386f8e0dfde086ecef124a6450c6195d89160" +checksum = "215ee31f8a88f588c349ce2d20108b2ed96089b96b9c2b03775dc35dd72938e8" dependencies = [ "base64 0.21.7", "pastey", @@ -454,7 +437,7 @@ dependencies = [ "proc-macro2", "quote", "syn", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -465,11 +448,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -511,6 +494,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "blowfish" version = "0.9.1" @@ -523,9 +515,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -533,9 +525,9 @@ dependencies = [ [[package]] name = "buffer-redux" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e8acf87c5b9f5897cd3ebb9a327f420e0cae9dd4e5c1d2e36f2c84c571a58f1" +checksum = "431a9cc8d7efa49bc326729264537f5e60affce816c66edf434350778c9f4f54" dependencies = [ "memchr", ] @@ -554,9 +546,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" dependencies = [ "serde", ] @@ -582,9 +574,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.38" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ "find-msvc-tools", "jobserver", @@ -603,9 +595,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" @@ -624,7 +616,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -677,9 +669,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.5.48" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" dependencies = [ "clap_builder", "clap_derive", @@ -687,9 +679,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" dependencies = [ "anstream", "anstyle", @@ -699,9 +691,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -711,9 +703,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cmac" @@ -925,9 +917,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1115,7 +1107,7 @@ dependencies = [ [[package]] name = "defguard_common" -version = "1.5.2" +version = "1.6.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -1135,7 +1127,7 @@ dependencies = [ "serde", "sqlx", "struct-patch", - "thiserror 2.0.16", + "thiserror 2.0.17", "tonic", "tracing", "utoipa", @@ -1186,7 +1178,7 @@ dependencies = [ "secrecy", "semver", "serde", - "serde_cbor_2", + "serde_cbor_2 0.12.0-dev", "serde_json", "serde_qs", "serde_urlencoded", @@ -1198,7 +1190,7 @@ dependencies = [ "strum", "strum_macros", "tera", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", "tokio", "tokio-stream", @@ -1232,7 +1224,7 @@ dependencies = [ "defguard_core", "serde_json", "sqlx", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1244,7 +1236,7 @@ dependencies = [ "defguard_core", "defguard_event_logger", "defguard_mail", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1263,7 +1255,7 @@ dependencies = [ "serde_json", "sqlx", "tera", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", ] @@ -1288,7 +1280,7 @@ dependencies = [ "os_info", "semver", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", "tonic", "tower", "tracing", @@ -1331,12 +1323,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1442,6 +1434,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1464,9 +1466,9 @@ dependencies = [ [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -1638,7 +1640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -1688,9 +1690,9 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "find-msvc-tools" -version = "0.1.2" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "fixedbitset" @@ -1700,9 +1702,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -1911,21 +1913,21 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "js-sys", "libc", "r-efi", - "wasi 0.14.7+wasi-0.2.4", + "wasip2", "wasm-bindgen", ] @@ -1939,19 +1941,13 @@ dependencies = [ "polyval", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "git2" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -1960,9 +1956,9 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -1977,7 +1973,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "ignore", "walkdir", ] @@ -2005,7 +2001,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.11.4", + "indexmap 2.12.0", "slab", "tokio", "tokio-util", @@ -2018,6 +2014,17 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2116,11 +2123,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2214,9 +2221,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2283,9 +2290,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" dependencies = [ "base64 0.22.1", "bytes", @@ -2333,9 +2340,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2346,9 +2353,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2359,11 +2366,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2374,42 +2380,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2455,9 +2457,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.23" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" dependencies = [ "crossbeam-deque", "globset", @@ -2482,9 +2484,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", "hashbrown 0.16.0", @@ -2501,17 +2503,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "libc", -] - [[package]] name = "ipnet" version = "2.11.0" @@ -2529,9 +2520,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" dependencies = [ "memchr", "serde", @@ -2539,9 +2530,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -2573,15 +2564,15 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -2687,9 +2678,9 @@ dependencies = [ [[package]] name = "lettre" -version = "0.11.18" +version = "0.11.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" +checksum = "9e13e10e8818f8b2a60f52cb127041d388b89f3a96a62be9ceaffa22262fef7f" dependencies = [ "async-trait", "base64 0.22.1", @@ -2715,9 +2706,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libgit2-sys" @@ -2743,7 +2734,7 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "redox_syscall", ] @@ -2769,9 +2760,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -2787,23 +2778,22 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] @@ -2895,9 +2885,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -2928,17 +2918,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -2978,6 +2969,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3005,11 +3008,11 @@ checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3024,11 +3027,10 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -3089,9 +3091,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -3099,9 +3101,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3138,11 +3140,170 @@ dependencies = [ "url", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "object" -version = "0.36.7" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -3176,9 +3337,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -3219,11 +3380,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -3251,9 +3412,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3282,14 +3443,18 @@ dependencies = [ [[package]] name = "os_info" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0e1ac5fde8d43c34139135df8ea9ee9465394b2d8d20f032d38998f64afffc3" +checksum = "7c39b5918402d564846d5aba164c09a66cc88d232179dfd3e3c619a25a268392" dependencies = [ + "android_system_properties", "log", - "plist", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", "serde", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3338,9 +3503,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3348,15 +3513,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -3404,12 +3569,12 @@ checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pem" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" dependencies = [ "base64 0.22.1", - "serde", + "serde_core", ] [[package]] @@ -3429,20 +3594,19 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" dependencies = [ "memchr", - "thiserror 2.0.16", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc58706f770acb1dbd0973e6530a3cff4746fb721207feb3a8a6064cd0b6c663" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" dependencies = [ "pest", "pest_generator", @@ -3450,9 +3614,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4f36811dfe07f7b8573462465d5cb8965fffc2e71ae377a33aecf14c2c9a2f" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" dependencies = [ "pest", "pest_meta", @@ -3463,9 +3627,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", "sha2", @@ -3478,7 +3642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.11.4", + "indexmap 2.12.0", ] [[package]] @@ -3660,19 +3824,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap 2.11.4", - "quick-xml", - "serde", - "time", -] - [[package]] name = "polyval" version = "0.6.2" @@ -3687,9 +3838,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3745,9 +3896,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -3814,10 +3965,11 @@ checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" [[package]] name = "psm" -version = "0.1.26" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] @@ -3837,7 +3989,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -3852,22 +4004,13 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "pulldown-cmark-to-cmark" -version = "21.0.0" +version = "21.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5b6a0769a491a08b31ea5c62494a8f144ee0987d86d670a8af4df1e1b7cde75" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" dependencies = [ "pulldown-cmark", ] -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.11.9" @@ -3882,7 +4025,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -3895,7 +4038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", - "getrandom 0.3.3", + "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", @@ -3903,7 +4046,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -3925,9 +4068,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -4006,32 +4149,32 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", ] [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -4040,9 +4183,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.2" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -4052,9 +4195,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -4063,15 +4206,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", @@ -4152,9 +4295,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" dependencies = [ "const-oid", "digest", @@ -4173,9 +4316,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4184,9 +4327,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", @@ -4197,9 +4340,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "globset", "sha2", @@ -4217,12 +4360,6 @@ dependencies = [ "trim-in-place", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -4253,18 +4390,18 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.32" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ "log", "once_cell", @@ -4277,21 +4414,21 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.0", + "security-framework 3.5.1", ] [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" dependencies = [ "web-time", "zeroize", @@ -4299,9 +4436,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.6" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", @@ -4335,7 +4472,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -4352,9 +4489,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" dependencies = [ "dyn-clone", "ref-cast", @@ -4399,7 +4536,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -4408,11 +4545,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -4441,9 +4578,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -4475,24 +4612,34 @@ version = "0.12.0-dev" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b46d75f449e01f1eddbe9b00f432d616fbbd899b809c837d0fbc380496a0dd55" dependencies = [ - "half", + "half 1.8.3", + "serde", +] + +[[package]] +name = "serde_cbor_2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aec2709de9078e077090abd848e967abab63c9fb3fdb5d4799ad359d8d482c" +dependencies = [ + "half 2.7.1", "serde", ] [[package]] name = "serde_core" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.226" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -4506,7 +4653,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" dependencies = [ "form_urlencoded", - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde_core", @@ -4553,7 +4700,7 @@ checksum = "f3faaf9e727533a19351a43cc5a8de957372163c7d35cc48c90b75cdda13c352" dependencies = [ "percent-encoding", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -4570,19 +4717,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c522100790450cf78eeac1507263d0a350d4d5b30df0c8e1fe051a10c22b376e" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.4", + "indexmap 2.12.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.1.0", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -4590,9 +4736,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.1" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327ada00f7d64abaac1e55a6911e90cf665aa051b9a561c7006c157f4633135e" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -4606,7 +4752,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "itoa", "ryu", "serde", @@ -4739,7 +4885,7 @@ checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" dependencies = [ "num-bigint", "num-traits", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -4797,12 +4943,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4856,7 +5002,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.4", + "indexmap 2.12.0", "ipnetwork", "log", "memchr", @@ -4867,7 +5013,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", "tracing", @@ -4921,7 +5067,7 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "bytes", "chrono", @@ -4951,7 +5097,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -4965,7 +5111,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "chrono", "crc", @@ -4991,7 +5137,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "uuid", "whoami", @@ -5017,7 +5163,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", "url", "uuid", @@ -5066,15 +5212,15 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stacker" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +checksum = "e1f8b29fb42aafcea4edeeb6b2f2d7ecd0d969c48b4cf0d2e64aafc471dd6e59" dependencies = [ "cc", "cfg-if", @@ -5174,9 +5320,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.106" +version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -5209,7 +5355,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5232,15 +5378,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.22.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -5256,9 +5402,9 @@ dependencies = [ [[package]] name = "tera" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" dependencies = [ "chrono", "chrono-tz", @@ -5273,7 +5419,7 @@ dependencies = [ "serde", "serde_json", "slug", - "unic-segment", + "unicode-segmentation", ] [[package]] @@ -5287,11 +5433,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -5307,9 +5453,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -5369,9 +5515,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -5394,28 +5540,25 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", - "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -5434,9 +5577,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.3" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5456,9 +5599,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -5469,20 +5612,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.6" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "toml_datetime", "toml_parser", "winnow", @@ -5490,9 +5633,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] @@ -5601,7 +5744,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", - "indexmap 2.11.4", + "indexmap 2.12.0", "pin-project-lite", "slab", "sync_wrapper", @@ -5618,7 +5761,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytes", "futures-core", "futures-util", @@ -5748,9 +5891,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uaparser" @@ -5772,56 +5915,6 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" -[[package]] -name = "unic-char-property" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" -dependencies = [ - "unic-char-range", -] - -[[package]] -name = "unic-char-range" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" - -[[package]] -name = "unic-common" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" - -[[package]] -name = "unic-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" -dependencies = [ - "unic-ucd-segment", -] - -[[package]] -name = "unic-ucd-segment" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" -dependencies = [ - "unic-char-property", - "unic-char-range", - "unic-ucd-version", -] - -[[package]] -name = "unic-ucd-version" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" -dependencies = [ - "unic-common", -] - [[package]] name = "unicase" version = "2.8.1" @@ -5836,30 +5929,36 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -5925,7 +6024,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.11.4", + "indexmap 2.12.0", "serde", "serde_json", "utoipa-gen", @@ -5975,7 +6074,7 @@ version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.3.4", "js-sys", "serde", "wasm-bindgen", @@ -6063,15 +6162,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" @@ -6089,9 +6179,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", @@ -6100,25 +6190,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.53" +version = "0.4.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" dependencies = [ "cfg-if", "js-sys", @@ -6129,9 +6205,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6139,22 +6215,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.103" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] @@ -6174,9 +6250,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.80" +version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -6206,9 +6282,9 @@ dependencies = [ [[package]] name = "webauthn-attestation-ca" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e43534efe4e8f56c4eb1615a27e24d2ff29281385c843cf9f16ac1077dbdc" +checksum = "f77a2892ec44032e6c48dad9aad1b05fada09c346ada11d8d32db119b4b4f205" dependencies = [ "base64urlsafedata", "openssl", @@ -6220,9 +6296,9 @@ dependencies = [ [[package]] name = "webauthn-authenticator-rs" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d11d7d7408e6c7cf65ab4d79b1f96c2a531df4e469e12656d6b814bdcd1b1" +checksum = "45f8fe3811c8d6c6830d263452670a608fd4dcdfc481349bd4d1e6a46d6c7a0f" dependencies = [ "async-stream", "async-trait", @@ -6238,7 +6314,7 @@ dependencies = [ "openssl-sys", "serde", "serde_bytes", - "serde_cbor_2", + "serde_cbor_2 0.13.0", "serde_json", "thiserror 1.0.69", "tokio", @@ -6253,9 +6329,9 @@ dependencies = [ [[package]] name = "webauthn-rs" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed1f861a94557baeb0cf711e3e55d623c46b68f4aab7aa932562f785b8b5f1ab" +checksum = "eb7c3a2f9c8bddd524e47bbd427bcf3a28aa074de55d74470b42a91a41937b8e" dependencies = [ "base64urlsafedata", "serde", @@ -6267,9 +6343,9 @@ dependencies = [ [[package]] name = "webauthn-rs-core" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "269c210cd5f183aaca860bb5733187d1dd110ebed54640f8fc1aca31a04aa4dc" +checksum = "19f1d80f3146382529fe70a3ab5d0feb2413a015204ed7843f9377cd39357fc4" dependencies = [ "base64 0.21.7", "base64urlsafedata", @@ -6281,7 +6357,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "serde", - "serde_cbor_2", + "serde_cbor_2 0.13.0", "serde_json", "thiserror 1.0.69", "tracing", @@ -6294,9 +6370,9 @@ dependencies = [ [[package]] name = "webauthn-rs-proto" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "144dbee9abb4bfad78fd283a2613f0312a0ed5955051b7864cfc98679112ae60" +checksum = "9e786894f89facb9aaf1c5f6559670236723c98382e045521c76f3d5ca5047bd" dependencies = [ "base64 0.21.7", "base64urlsafedata", @@ -6307,9 +6383,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" dependencies = [ "rustls-pki-types", ] @@ -6330,27 +6406,27 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result", + "windows-strings", ] [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -6359,9 +6435,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -6376,55 +6452,37 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-result" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.1", + "windows-result", + "windows-strings", ] [[package]] name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.1.3", + "windows-link 0.2.1", ] [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -6460,16 +6518,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -6505,19 +6563,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -6534,9 +6592,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -6552,9 +6610,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -6570,9 +6628,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -6582,9 +6640,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -6600,9 +6658,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -6618,9 +6676,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -6636,9 +6694,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -6654,9 +6712,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -6675,9 +6733,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -6728,11 +6786,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -6740,9 +6797,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -6793,9 +6850,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] @@ -6813,9 +6870,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -6824,9 +6881,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -6835,9 +6892,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", @@ -6853,7 +6910,7 @@ dependencies = [ "arbitrary", "crc32fast", "flate2", - "indexmap 2.11.4", + "indexmap 2.12.0", "memchr", "zopfli", ] @@ -6866,9 +6923,9 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", diff --git a/Dockerfile b/Dockerfile index 4da63c50ff..8939f2b525 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,7 +44,7 @@ RUN cargo install --locked --bin defguard --path ./crates/defguard --root /build # run FROM public.ecr.aws/docker/library/debian:13-slim -RUN apt-get update -y && \ +RUN apt-get update -y && apt-get upgrade -y && \ apt-get install --no-install-recommends -y ca-certificates libssl-dev && \ rm -rf /var/lib/apt/lists/* WORKDIR /app diff --git a/crates/defguard_common/Cargo.toml b/crates/defguard_common/Cargo.toml index 2bc3053fcf..1855788cb0 100644 --- a/crates/defguard_common/Cargo.toml +++ b/crates/defguard_common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "defguard_common" -version = "1.5.2" +version = "1.6.0" edition.workspace = true license-file.workspace = true homepage.workspace = true diff --git a/crates/defguard_common/src/db/models/authentication_key.rs b/crates/defguard_common/src/db/models/authentication_key.rs index a1bc189586..78b44b3c58 100644 --- a/crates/defguard_common/src/db/models/authentication_key.rs +++ b/crates/defguard_common/src/db/models/authentication_key.rs @@ -6,7 +6,7 @@ use sqlx::{Error as SqlxError, PgExecutor, Type, query_as}; use crate::db::{Id, NoId}; -#[derive(Clone, Debug, Deserialize, Serialize, Type)] +#[derive(Clone, Debug, Deserialize, Serialize, Type, PartialEq)] #[sqlx(type_name = "authentication_key_type", rename_all = "lowercase")] #[serde(rename_all = "lowercase")] pub enum AuthenticationKeyType { @@ -23,7 +23,7 @@ impl Display for AuthenticationKeyType { } } -#[derive(Clone, Debug, Deserialize, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, PartialEq)] #[table(authentication_key)] pub struct AuthenticationKey { pub id: I, diff --git a/crates/defguard_common/src/db/models/settings.rs b/crates/defguard_common/src/db/models/settings.rs index e2ae4fee63..018aa0b913 100644 --- a/crates/defguard_common/src/db/models/settings.rs +++ b/crates/defguard_common/src/db/models/settings.rs @@ -1,6 +1,5 @@ use std::{collections::HashMap, fmt}; -use crate::{global_value, secret::SecretStringWrapper}; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, PgPool, Type, query, query_as}; use struct_patch::Patch; @@ -8,6 +7,8 @@ use thiserror::Error; use tracing::{debug, info, warn}; use uuid::Uuid; +use crate::{global_value, secret::SecretStringWrapper}; + global_value!(SETTINGS, Option, None, set_settings, get_settings); /// Initializes global `SETTINGS` struct at program startup diff --git a/crates/defguard_core/src/db/models/activity_log/mod.rs b/crates/defguard_core/src/db/models/activity_log/mod.rs index 62cf59154c..cf9314cf42 100644 --- a/crates/defguard_core/src/db/models/activity_log/mod.rs +++ b/crates/defguard_core/src/db/models/activity_log/mod.rs @@ -1,10 +1,9 @@ use chrono::NaiveDateTime; +use defguard_common::db::{Id, NoId}; use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{FromRow, Type}; -use defguard_common::db::{Id, NoId}; - pub mod metadata; #[derive(Clone, Debug, Deserialize, Serialize, Type)] diff --git a/crates/defguard_core/src/db/models/device.rs b/crates/defguard_core/src/db/models/device.rs index 00c895ff15..f359eb6383 100644 --- a/crates/defguard_core/src/db/models/device.rs +++ b/crates/defguard_core/src/db/models/device.rs @@ -3,7 +3,7 @@ use std::{fmt, net::IpAddr}; use base64::{Engine, prelude::BASE64_STANDARD}; #[cfg(test)] use chrono::NaiveDate; -use chrono::{NaiveDateTime, Utc}; +use chrono::{NaiveDateTime, Timelike, Utc}; use defguard_common::{ csv::AsCsv, db::{Id, NoId, models::ModelError}, @@ -26,7 +26,10 @@ use utoipa::ToSchema; use super::wireguard::{ LocationMfaMode, NetworkAddressError, WIREGUARD_MAX_HANDSHAKE, WireguardNetwork, }; -use crate::{KEY_LENGTH, db::User}; +use crate::{ + KEY_LENGTH, + db::{User, models::wireguard::ServiceLocationMode}, +}; #[derive(Serialize, ToSchema)] pub struct DeviceConfig { @@ -42,6 +45,7 @@ pub struct DeviceConfig { pub(crate) dns: Option, pub(crate) keepalive_interval: i32, pub(crate) location_mfa_mode: LocationMfaMode, + pub(crate) service_location_mode: ServiceLocationMode, } // The type of a device: @@ -501,7 +505,8 @@ impl WireguardNetworkDevice { WireguardNetwork, "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, \ - acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM wireguard_network WHERE id = $1", self.wireguard_network_id ) @@ -532,12 +537,20 @@ impl Device { description: Option, configured: bool, ) -> Self { + // FIXME: this is a workaround for reducing timestamp precision. + // `chrono` has nanosecond precision by default, while Postgres only does microseconds. + // It avoids issues when comparing to objects fetched from DB. + let created = Utc::now().naive_utc(); + let created = created + .with_nanosecond((created.nanosecond() / 1_000) * 1_000) + .expect("failed to truncate timestamp precision"); + Self { id: NoId, name, wireguard_pubkey, user_id, - created: Utc::now().naive_utc(), + created, device_type, description, configured, @@ -695,6 +708,7 @@ impl Device { dns: network.dns.clone(), keepalive_interval: network.keepalive_interval, location_mfa_mode: network.location_mfa_mode.clone(), + service_location_mode: network.service_location_mode.clone(), }; Ok((device_network_info, device_config)) @@ -728,6 +742,7 @@ impl Device { dns: network.dns.clone(), keepalive_interval: network.keepalive_interval, location_mfa_mode: network.location_mfa_mode.clone(), + service_location_mode: network.service_location_mode.clone(), }; Ok((device_network_info, device_config)) @@ -790,6 +805,7 @@ impl Device { dns: network.dns, keepalive_interval: network.keepalive_interval, location_mfa_mode: network.location_mfa_mode.clone(), + service_location_mode: network.service_location_mode.clone(), }); } } @@ -936,7 +952,8 @@ impl Device { WireguardNetwork, "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, \ - acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM wireguard_network WHERE id IN \ (SELECT wireguard_network_id FROM wireguard_network_device WHERE device_id = $1 ORDER BY id LIMIT 1)", self.id @@ -998,7 +1015,7 @@ impl Device { "SELECT id, username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE id = $1", self.user_id ).fetch_one(executor).await diff --git a/crates/defguard_core/src/db/models/enrollment.rs b/crates/defguard_core/src/db/models/enrollment.rs index 7c9237c3ca..e0e0824d2e 100644 --- a/crates/defguard_core/src/db/models/enrollment.rs +++ b/crates/defguard_core/src/db/models/enrollment.rs @@ -394,7 +394,7 @@ impl User { /// This creates a new enrollment token valid for 24h /// and optionally sends enrollment email notification to user pub async fn start_enrollment( - &self, + &mut self, transaction: &mut PgConnection, admin: &User, email: Option, @@ -447,6 +447,11 @@ impl User { enrollment.id, self.username ); + // Mark the user with enrollment-pending flag. + // https://github.com/DefGuard/client/issues/647 + self.enrollment_pending = true; + self.save(&mut *transaction).await?; + if send_user_notification { if let Some(email) = email { debug!( diff --git a/crates/defguard_core/src/db/models/group.rs b/crates/defguard_core/src/db/models/group.rs index b9734fd4f6..017075934c 100644 --- a/crates/defguard_core/src/db/models/group.rs +++ b/crates/defguard_core/src/db/models/group.rs @@ -85,7 +85,7 @@ impl Group { "SELECT \"user\".id, username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" \ JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", diff --git a/crates/defguard_core/src/db/models/oauth2authorizedapp.rs b/crates/defguard_core/src/db/models/oauth2authorizedapp.rs index 0039265a65..0b7bb1af90 100644 --- a/crates/defguard_core/src/db/models/oauth2authorizedapp.rs +++ b/crates/defguard_core/src/db/models/oauth2authorizedapp.rs @@ -1,8 +1,7 @@ +use defguard_common::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, PgPool, query_as}; -use defguard_common::db::{Id, NoId}; - #[derive(Model)] pub struct OAuth2AuthorizedApp { pub id: I, diff --git a/crates/defguard_core/src/db/models/oauth2client.rs b/crates/defguard_core/src/db/models/oauth2client.rs index 5f695618b5..d7e2cbdb53 100644 --- a/crates/defguard_core/src/db/models/oauth2client.rs +++ b/crates/defguard_core/src/db/models/oauth2client.rs @@ -1,13 +1,14 @@ +use defguard_common::{ + db::{Id, NoId}, + random::gen_alphanumeric, +}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query_as}; -use crate::db::OAuth2Token; - use super::NewOpenIDClient; -use defguard_common::db::{Id, NoId}; -use defguard_common::random::gen_alphanumeric; +use crate::db::OAuth2Token; -#[derive(Clone, Debug, Deserialize, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, PartialEq)] pub struct OAuth2Client { pub id: I, pub client_id: String, // unique diff --git a/crates/defguard_core/src/db/models/polling_token.rs b/crates/defguard_core/src/db/models/polling_token.rs index 8b19e98dda..b4d911936d 100644 --- a/crates/defguard_core/src/db/models/polling_token.rs +++ b/crates/defguard_core/src/db/models/polling_token.rs @@ -1,10 +1,11 @@ use chrono::{NaiveDateTime, Utc}; +use defguard_common::{ + db::{Id, NoId}, + random::gen_alphanumeric, +}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query_as}; -use defguard_common::db::{Id, NoId}; -use defguard_common::random::gen_alphanumeric; - // Token used for polling requests. #[derive(Clone, Debug, Model)] pub struct PollingToken { diff --git a/crates/defguard_core/src/db/models/session.rs b/crates/defguard_core/src/db/models/session.rs index 52d7a0fd9b..ee1cda00b0 100644 --- a/crates/defguard_core/src/db/models/session.rs +++ b/crates/defguard_core/src/db/models/session.rs @@ -1,10 +1,9 @@ use chrono::{NaiveDateTime, TimeDelta, Utc}; use defguard_common::{config::server_config, db::Id, random::gen_alphanumeric}; +use defguard_mail::templates::SessionContext; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; use webauthn_rs::prelude::{PasskeyAuthentication, PasskeyRegistration}; -use defguard_mail::templates::SessionContext; - #[derive(Clone, PartialEq, Type)] #[repr(i16)] pub enum SessionState { diff --git a/crates/defguard_core/src/db/models/user.rs b/crates/defguard_core/src/db/models/user.rs index 6ba075b646..f545ed850d 100644 --- a/crates/defguard_core/src/db/models/user.rs +++ b/crates/defguard_core/src/db/models/user.rs @@ -8,6 +8,11 @@ use argon2::{ }, }; use axum::http::StatusCode; +use defguard_common::{ + config::server_config, + db::{Id, NoId, models::MFAMethod}, + random::{gen_alphanumeric, gen_totp_secret}, +}; use defguard_mail::templates::UserContext; use model_derive::Model; #[cfg(test)] @@ -36,11 +41,6 @@ use crate::{ error::WebError, grpc::gateway::{send_multiple_wireguard_events, send_wireguard_event}, }; -use defguard_common::{ - config::server_config, - db::{Id, NoId, models::MFAMethod}, - random::{gen_alphanumeric, gen_totp_secret}, -}; const RECOVERY_CODES_COUNT: usize = 8; @@ -96,6 +96,10 @@ pub struct User { pub(crate) mfa_method: MFAMethod, #[model(ref)] pub(crate) recovery_codes: Vec, + /// Indicates that an administrator has requested an enrollment token for this user. + /// Uninitialized clients should then guide the user through enrollment process. + /// Related issue: https://github.com/DefGuard/client/issues/647. + pub enrollment_pending: bool, } // TODO: Refactor the user struct to use SecretStringWrapper instead of this @@ -122,6 +126,7 @@ impl fmt::Debug for User { email_mfa_secret: _, mfa_method, recovery_codes, + enrollment_pending, } = self; f.debug_struct("User") @@ -148,6 +153,7 @@ impl fmt::Debug for User { .field("password_hash", &"***") .field("totp_secret", &"***") .field("email_mfa_secret", &"***") + .field("enrollment_pending", enrollment_pending) .finish() } } @@ -201,6 +207,7 @@ impl User { ldap_pass_randomized: false, ldap_rdn: Some(username.clone()), ldap_user_path: None, + enrollment_pending: false, } } } @@ -237,12 +244,17 @@ impl User { format!("{} {}", self.first_name, self.last_name) } - /// Check if user is enrolled. - /// We assume the user is enrolled if they have a password set - /// or they have logged in using an external OIDC. + /// Determines whether the user is considered enrolled. + /// + /// A user is treated as enrolled if: + /// - The `enrollment_pending` flag is **not** set, i.e. enrollment was not requested by an + /// administrator (https://github.com/DefGuard/client/issues/647). + /// - They either have a password configured, have authenticated via an external OIDC provider + /// or were synced from LDAP. #[must_use] pub fn is_enrolled(&self) -> bool { - self.password_hash.is_some() || self.openid_sub.is_some() || self.from_ldap + !self.enrollment_pending + && (self.password_hash.is_some() || self.openid_sub.is_some() || self.from_ldap) } #[must_use] @@ -675,7 +687,7 @@ impl User { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" \ INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id \ INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id \ @@ -826,7 +838,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE username = $1", username ) @@ -846,7 +858,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE email ILIKE $1", email ) @@ -880,7 +892,8 @@ impl User { query_as( "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ - mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, \ + ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE email = ANY($1)", ) .bind(emails) @@ -900,7 +913,7 @@ impl User { "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE openid_sub = $1", sub ) @@ -1129,8 +1142,9 @@ impl User { Self, "SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, \ u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, \ - u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, \ + u.is_active, u.openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, \ + enrollment_pending \ FROM \"user\" u \ JOIN \"device\" d ON u.id = d.user_id \ WHERE d.id = $1", @@ -1152,7 +1166,8 @@ impl User { query_as( "SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ - mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + mfa_method, recovery_codes, is_active, openid_sub, from_ldap, ldap_pass_randomized, \ + ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE email NOT IN (SELECT * FROM UNNEST($1::TEXT[]))", ) .bind(user_emails) @@ -1181,7 +1196,7 @@ impl User { SELECT u.id, u.username, u.password_hash, u.last_name, u.first_name, u.email, \ u.phone, u.mfa_enabled, u.totp_enabled, u.email_mfa_enabled, \ u.totp_secret, u.email_mfa_secret, u.mfa_method \"mfa_method: _\", u.recovery_codes, u.is_active, u.openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" u \ WHERE EXISTS (SELECT 1 FROM group_user gu LEFT JOIN \"group\" g ON gu.group_id = g.id \ WHERE is_admin = true AND user_id = u.id) AND u.is_active = true" @@ -1227,6 +1242,7 @@ impl Distribution> for Standard { ldap_pass_randomized: false, ldap_rdn: None, ldap_user_path: None, + enrollment_pending: false, } } } @@ -1267,6 +1283,7 @@ impl Distribution> for Standard { ldap_pass_randomized: false, ldap_rdn: None, ldap_user_path: None, + enrollment_pending: false, } } } @@ -1275,12 +1292,11 @@ impl Distribution> for Standard { mod test { use defguard_common::{ config::{DefGuardConfig, SERVER_CONFIG}, - db::setup_pool, + db::{models::settings::initialize_current_settings, setup_pool}, }; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; use super::*; - use defguard_common::db::models::settings::initialize_current_settings; #[sqlx::test] async fn test_mfa_code(_: PgPoolOptions, options: PgConnectOptions) { @@ -1626,4 +1642,60 @@ mod test { assert_eq!(users[0].id, user1.id); assert_eq!(users[1].id, albus.id); } + + #[sqlx::test] + async fn test_user_is_enrolled(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let user = User::new( + "test", + Some("31071980"), + "harry", + "potter", + "harry@hogwart.edu.uk", + None, + ); + let mut user = user.save(&pool).await.unwrap(); + + user.enrollment_pending = false; + user.password_hash = Some(hash_password("31071980").unwrap()); + user.openid_sub = Some("sub".to_string()); + user.from_ldap = true; + user.save(&pool).await.unwrap(); + assert!(user.is_enrolled()); + + user.enrollment_pending = false; + user.password_hash = None; + user.openid_sub = Some("sub".to_string()); + user.from_ldap = true; + user.save(&pool).await.unwrap(); + assert!(user.is_enrolled()); + + user.enrollment_pending = false; + user.password_hash = None; + user.openid_sub = None; + user.from_ldap = true; + user.save(&pool).await.unwrap(); + assert!(user.is_enrolled()); + + user.enrollment_pending = false; + user.password_hash = None; + user.openid_sub = None; + user.from_ldap = false; + user.save(&pool).await.unwrap(); + assert!(!user.is_enrolled()); + + user.enrollment_pending = true; + user.password_hash = None; + user.openid_sub = None; + user.from_ldap = false; + user.save(&pool).await.unwrap(); + assert!(!user.is_enrolled()); + + user.enrollment_pending = true; + user.password_hash = Some(hash_password("31071980").unwrap()); + user.openid_sub = Some("sub".to_string()); + user.from_ldap = true; + user.save(&pool).await.unwrap(); + assert!(!user.is_enrolled()); + } } diff --git a/crates/defguard_core/src/db/models/webauthn.rs b/crates/defguard_core/src/db/models/webauthn.rs index 9b4407d67f..bd5a912f70 100644 --- a/crates/defguard_core/src/db/models/webauthn.rs +++ b/crates/defguard_core/src/db/models/webauthn.rs @@ -1,10 +1,9 @@ +use defguard_common::db::{Id, NoId, models::ModelError}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, query, query_as, query_scalar}; use webauthn_rs::prelude::Passkey; -use defguard_common::db::{Id, NoId, models::ModelError}; - -#[derive(Model, Clone, Debug)] +#[derive(Model, Clone, Debug, PartialEq)] pub struct WebAuthn { pub id: I, pub user_id: Id, diff --git a/crates/defguard_core/src/db/models/webhook.rs b/crates/defguard_core/src/db/models/webhook.rs index f45d9831d7..8b2715c46a 100644 --- a/crates/defguard_core/src/db/models/webhook.rs +++ b/crates/defguard_core/src/db/models/webhook.rs @@ -1,8 +1,8 @@ +use defguard_common::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, FromRow, PgPool, query_as}; use super::UserInfo; -use defguard_common::db::{Id, NoId}; /// App events which triggers webhook action #[derive(Debug)] @@ -47,7 +47,7 @@ impl AppEvent { } } -#[derive(Clone, Debug, Deserialize, FromRow, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, FromRow, Model, Serialize, PartialEq)] pub struct WebHook { pub id: I, pub url: String, diff --git a/crates/defguard_core/src/db/models/wireguard.rs b/crates/defguard_core/src/db/models/wireguard.rs index fef77250f3..33c26e4989 100644 --- a/crates/defguard_core/src/db/models/wireguard.rs +++ b/crates/defguard_core/src/db/models/wireguard.rs @@ -12,6 +12,13 @@ use defguard_common::{ csv::AsCsv, db::{Id, NoId, models::ModelError}, }; +use defguard_proto::{ + enterprise::firewall::FirewallConfig, + gateway::Peer, + proxy::{ + LocationMfaMode as ProtoLocationMfaMode, ServiceLocationMode as ProtoServiceLocationMode, + }, +}; use ipnetwork::{IpNetwork, IpNetworkError, NetworkSize}; use model_derive::Model; use rand::rngs::OsRng; @@ -33,14 +40,10 @@ use super::{ wireguard_peer_stats::WireguardPeerStats, }; use crate::{ - enterprise::firewall::FirewallError, + enterprise::{firewall::FirewallError, is_enterprise_enabled}, grpc::gateway::{send_multiple_wireguard_events, state::GatewayState}, wg_config::ImportedDevice, }; -use defguard_proto::{ - enterprise::firewall::FirewallConfig, gateway::Peer, - proxy::LocationMfaMode as ProtoLocationMfaMode, -}; pub const DEFAULT_KEEPALIVE_INTERVAL: i32 = 25; pub const DEFAULT_DISCONNECT_THRESHOLD: i32 = 300; @@ -126,8 +129,40 @@ impl From for ProtoLocationMfaMode { } } +#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, ToSchema, Type)] +#[sqlx(type_name = "service_location_mode", rename_all = "lowercase")] +#[serde(rename_all = "lowercase")] +pub enum ServiceLocationMode { + #[default] + Disabled, + PreLogon, + AlwaysOn, +} + +impl From for ServiceLocationMode { + fn from(value: ProtoServiceLocationMode) -> Self { + match value { + ProtoServiceLocationMode::Unspecified | ProtoServiceLocationMode::Disabled => { + ServiceLocationMode::Disabled + } + ProtoServiceLocationMode::Prelogon => ServiceLocationMode::PreLogon, + ProtoServiceLocationMode::Alwayson => ServiceLocationMode::AlwaysOn, + } + } +} + +impl From for ProtoServiceLocationMode { + fn from(value: ServiceLocationMode) -> Self { + match value { + ServiceLocationMode::Disabled => ProtoServiceLocationMode::Disabled, + ServiceLocationMode::PreLogon => ProtoServiceLocationMode::Prelogon, + ServiceLocationMode::AlwaysOn => ProtoServiceLocationMode::Alwayson, + } + } +} + /// Stores configuration required to setup a WireGuard network -#[derive(Clone, Debug, Deserialize, Eq, Hash, Model, PartialEq, Serialize, ToSchema)] +#[derive(Clone, Deserialize, Eq, Hash, Model, PartialEq, Serialize, ToSchema)] #[table(wireguard_network)] pub struct WireguardNetwork { pub id: I, @@ -151,6 +186,8 @@ pub struct WireguardNetwork { pub peer_disconnect_threshold: i32, #[model(enum)] pub location_mfa_mode: LocationMfaMode, + #[model(enum)] + pub service_location_mode: ServiceLocationMode, } pub struct WireguardKey { @@ -170,6 +207,29 @@ impl fmt::Display for WireguardNetwork { } } +impl fmt::Debug for WireguardNetwork { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("WireguardNetwork") + .field("id", &self.id) + .field("name", &self.name) + .field("address", &self.address) + .field("port", &self.port) + .field("pubkey", &self.pubkey) + .field("prvkey", &"***") + .field("endpoint", &self.endpoint) + .field("dns", &self.dns) + .field("allowed_ips", &self.allowed_ips) + .field("connected_at", &self.connected_at) + .field("acl_enabled", &self.acl_enabled) + .field("acl_default_allow", &self.acl_default_allow) + .field("keepalive_interval", &self.keepalive_interval) + .field("peer_disconnect_threshold", &self.peer_disconnect_threshold) + .field("location_mfa_mode", &self.location_mfa_mode) + .field("service_location_mode", &self.service_location_mode) + .finish() + } +} + #[cfg(test)] impl Default for WireguardNetwork { fn default() -> Self { @@ -189,6 +249,7 @@ impl Default for WireguardNetwork { acl_default_allow: false, acl_enabled: false, location_mfa_mode: LocationMfaMode::default(), + service_location_mode: ServiceLocationMode::default(), } } } @@ -249,6 +310,7 @@ impl WireguardNetwork { acl_enabled: bool, acl_default_allow: bool, location_mfa_mode: LocationMfaMode, + service_location_mode: ServiceLocationMode, ) -> Self { let prvkey = StaticSecret::random_from_rng(OsRng); let pubkey = PublicKey::from(&prvkey); @@ -269,6 +331,7 @@ impl WireguardNetwork { acl_enabled, acl_default_allow, location_mfa_mode, + service_location_mode, } } @@ -299,7 +362,8 @@ impl WireguardNetwork { WireguardNetwork, "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, \ - acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM wireguard_network WHERE name = $1", name ) @@ -343,18 +407,22 @@ impl WireguardNetwork { &self, device_count: usize, ) -> Result<(), WireguardNetworkError> { - debug!("Checking if {device_count} devices can fit in network {self}"); - let network_size = self.address[0].size(); - // include address, network, and broadcast in the calculation - match network_size { - NetworkSize::V4(size) => { - if device_count as u32 > size { - return Err(WireguardNetworkError::NetworkTooSmall); + debug!("Checking if {device_count} devices can fit in networks used by location {self}"); + // if given location uses multiple subnets validate devices can fit them all + for subnet in &self.address { + debug!("Checking if {device_count} devices can fit in network {subnet}"); + let network_size = subnet.size(); + // include address, network, and broadcast in the calculation + match network_size { + NetworkSize::V4(size) => { + if device_count as u32 > size { + return Err(WireguardNetworkError::NetworkTooSmall); + } } - } - NetworkSize::V6(size) => { - if device_count as u128 > size { - return Err(WireguardNetworkError::NetworkTooSmall); + NetworkSize::V6(size) => { + if device_count as u128 > size { + return Err(WireguardNetworkError::NetworkTooSmall); + } } } } @@ -1238,7 +1306,8 @@ impl WireguardNetwork { WireguardNetwork, "SELECT id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, acl_enabled, \ - acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM wireguard_network WHERE location_mfa_mode = 'external'::location_mfa_mode", ) .fetch_all(executor) @@ -1261,6 +1330,13 @@ impl WireguardNetwork { Ok(token) } + + /// If this location is marked as a service location, checks if all requirements are met for it to function: + /// - Enterprise is enabled + #[must_use] + pub fn should_prevent_service_location_usage(&self) -> bool { + self.service_location_mode != ServiceLocationMode::Disabled && !is_enterprise_enabled() + } } // [`IpNetwork`] does not implement [`Default`] @@ -1282,6 +1358,7 @@ impl Default for WireguardNetwork { acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode::default(), + service_location_mode: ServiceLocationMode::default(), } } } @@ -1418,7 +1495,7 @@ pub(crate) async fn networks_stats( mod test { use std::str::FromStr; - use chrono::{SubsecRound, TimeDelta}; + use chrono::{SubsecRound, TimeDelta, Utc}; use defguard_common::db::setup_pool; use matches::assert_matches; use sqlx::postgres::{PgConnectOptions, PgPoolOptions}; @@ -1995,6 +2072,7 @@ mod test { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -2126,6 +2204,7 @@ mod test { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -2245,4 +2324,167 @@ mod test { Err(NetworkAddressError::IsBroadcastAddress(..)) ); } + + #[sqlx::test] + async fn test_get_peers_service_location_modes(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let user = User::new( + "testuser", + Some("password123"), + "Test", + "User", + "test@example.com", + None, + ) + .save(&pool) + .await + .unwrap(); + + let device1 = Device::new( + "device1".into(), + "pubkey1".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + let device2 = Device::new( + "device2".into(), + "pubkey2".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + // Normal location (service_location_mode = Disabled) should return peers + let mut network_normal = WireguardNetwork { + name: "normal-location".to_string(), + service_location_mode: ServiceLocationMode::Disabled, + location_mfa_mode: LocationMfaMode::Disabled, + ..Default::default() + }; + network_normal.try_set_address("10.1.1.1/24").unwrap(); + let network_normal = network_normal.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network_normal.id, + device1.id, + vec![IpAddr::from_str("10.1.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + let peers_normal = network_normal.get_peers(&pool).await.unwrap(); + assert_eq!(peers_normal.len(), 1, "Normal location should return peers"); + assert_eq!(peers_normal[0].pubkey, "pubkey1"); + + // Service location with PreLogon mode returns peers when enterprise is enabled (test env default) + let mut network_prelogon = WireguardNetwork { + name: "prelogon-service-location".to_string(), + service_location_mode: ServiceLocationMode::PreLogon, + location_mfa_mode: LocationMfaMode::Disabled, + ..Default::default() + }; + network_prelogon.try_set_address("10.2.1.1/24").unwrap(); + let network_prelogon = network_prelogon.save(&pool).await.unwrap(); + + WireguardNetworkDevice::new( + network_prelogon.id, + device2.id, + vec![IpAddr::from_str("10.2.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + // PreLogon service location should return peers when enterprise is enabled + let peers_prelogon = network_prelogon.get_peers(&pool).await.unwrap(); + assert_eq!( + peers_prelogon.len(), + 1, + "PreLogon service location should return peers when enterprise is enabled" + ); + assert_eq!(peers_prelogon[0].pubkey, "pubkey2"); + + // Service location with AlwaysOn mode also returns peers when enterprise is enabled + let mut network_alwayson = WireguardNetwork { + name: "alwayson-service-location".to_string(), + service_location_mode: ServiceLocationMode::AlwaysOn, + location_mfa_mode: LocationMfaMode::Disabled, + ..Default::default() + }; + network_alwayson.try_set_address("10.3.1.1/24").unwrap(); + let network_alwayson = network_alwayson.save(&pool).await.unwrap(); + + let device3 = Device::new( + "device3".into(), + "pubkey3".into(), + user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + WireguardNetworkDevice::new( + network_alwayson.id, + device3.id, + vec![IpAddr::from_str("10.3.1.2").unwrap()], + ) + .insert(&pool) + .await + .unwrap(); + + // AlwaysOn service location should return peers when enterprise is enabled + let peers_alwayson = network_alwayson.get_peers(&pool).await.unwrap(); + assert_eq!( + peers_alwayson.len(), + 1, + "AlwaysOn service location should return peers when enterprise is enabled" + ); + assert_eq!(peers_alwayson[0].pubkey, "pubkey3"); + + // Now test the negative case: service locations with enterprise disabled + // Exceed the enterprise limits to disable enterprise features + use crate::enterprise::limits::{Counts, DEFAULT_LOCATIONS_LIMIT, set_counts}; + let over_limit_counts = Counts::new(1, 1, DEFAULT_LOCATIONS_LIMIT + 1, 0); + set_counts(over_limit_counts); + + // Test that normal location still returns peers even without enterprise + let peers_normal_no_ent = network_normal.get_peers(&pool).await.unwrap(); + assert_eq!( + peers_normal_no_ent.len(), + 1, + "Normal location should still return peers without enterprise" + ); + + // Test that PreLogon service location returns NO peers without enterprise + let peers_prelogon_no_ent = network_prelogon.get_peers(&pool).await.unwrap(); + assert!( + peers_prelogon_no_ent.is_empty(), + "PreLogon service location should return NO peers when enterprise is disabled" + ); + + // Test that AlwaysOn service location returns NO peers without enterprise + let peers_alwayson_no_ent = network_alwayson.get_peers(&pool).await.unwrap(); + assert!( + peers_alwayson_no_ent.is_empty(), + "AlwaysOn service location should return NO peers when enterprise is disabled" + ); + + let normal_counts = Counts::new(0, 0, 0, 0); + set_counts(normal_counts); + } } diff --git a/crates/defguard_core/src/db/models/wireguard_peer_stats.rs b/crates/defguard_core/src/db/models/wireguard_peer_stats.rs index 2323584c7d..350f826fdd 100644 --- a/crates/defguard_core/src/db/models/wireguard_peer_stats.rs +++ b/crates/defguard_core/src/db/models/wireguard_peer_stats.rs @@ -1,13 +1,12 @@ use std::time::Duration; use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc}; +use defguard_common::db::{Id, NoId}; use humantime::format_duration; use ipnetwork::IpNetwork; use model_derive::Model; use sqlx::{PgExecutor, PgPool, query, query_as, query_scalar}; -use defguard_common::db::{Id, NoId}; - #[derive(Debug, Deserialize, Model, Serialize)] #[table(wireguard_peer_stats)] pub struct WireguardPeerStats { diff --git a/crates/defguard_core/src/db/models/yubikey.rs b/crates/defguard_core/src/db/models/yubikey.rs index 0813319334..b12f4b8548 100644 --- a/crates/defguard_core/src/db/models/yubikey.rs +++ b/crates/defguard_core/src/db/models/yubikey.rs @@ -1,8 +1,7 @@ +use defguard_common::db::{Id, NoId}; use model_derive::Model; use sqlx::{PgExecutor, query, query_as}; -use defguard_common::db::{Id, NoId}; - #[derive(Deserialize, Model, Serialize)] pub struct YubiKey { pub id: I, diff --git a/crates/defguard_core/src/enterprise/db/models/acl.rs b/crates/defguard_core/src/enterprise/db/models/acl.rs index faddd5c08f..069a9ebe81 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl.rs @@ -18,7 +18,10 @@ use thiserror::Error; use crate::{ DeviceType, appstate::AppState, - db::{Device, GatewayEvent, Group, User, WireguardNetwork, models::wireguard::LocationMfaMode}, + db::{ + Device, GatewayEvent, Group, User, WireguardNetwork, + models::wireguard::{LocationMfaMode, ServiceLocationMode}, + }, enterprise::{ firewall::FirewallError, handlers::acl::{ApiAclAlias, ApiAclRule, EditAclAlias, EditAclRule}, @@ -906,7 +909,8 @@ impl AclRule { WireguardNetwork, "SELECT n.id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, \ - acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM aclrulenetwork r \ JOIN wireguard_network n \ ON n.id = r.network_id \ @@ -969,7 +973,7 @@ impl AclRule { "SELECT u.id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM aclruleuser r \ JOIN \"user\" u \ ON u.id = r.user_id \ @@ -995,7 +999,7 @@ impl AclRule { "SELECT u.id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM aclruleuser r \ JOIN \"user\" u \ ON u.id = r.user_id \ @@ -1176,7 +1180,7 @@ impl AclRuleInfo { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" \ WHERE is_active = true" ) @@ -1198,7 +1202,7 @@ impl AclRuleInfo { "SELECT id, username, password_hash, last_name, first_name, email, phone, mfa_enabled, \ totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" u \ JOIN group_user gu ON u.id=gu.user_id \ WHERE u.is_active=true AND gu.group_id=ANY($1)", @@ -1237,7 +1241,7 @@ impl AclRuleInfo { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, from_ldap, \ - ldap_pass_randomized, ldap_rdn, ldap_user_path \ + ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" \ WHERE is_active = true" ) @@ -1260,7 +1264,7 @@ impl AclRuleInfo { phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" u \ JOIN group_user gu ON u.id=gu.user_id \ WHERE u.is_active=true AND gu.group_id=ANY($1)", diff --git a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs index becdc26481..85cdd42d82 100644 --- a/crates/defguard_core/src/enterprise/db/models/acl/tests.rs +++ b/crates/defguard_core/src/enterprise/db/models/acl/tests.rs @@ -183,6 +183,7 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -199,6 +200,7 @@ async fn test_rule_relations(_: PgPoolOptions, options: PgConnectOptions) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await diff --git a/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs b/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs index 2d472bec2c..3548de1883 100644 --- a/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs +++ b/crates/defguard_core/src/enterprise/db/models/activity_log_stream.rs @@ -1,15 +1,15 @@ +use defguard_common::{ + db::{Id, NoId}, + secret::SecretStringWrapper, +}; use model_derive::Model; use serde::Serialize; use sqlx::{Error as SqlxError, FromRow, PgExecutor, Type, query_as}; use strum_macros::{Display, EnumString}; use crate::enterprise::activity_log_stream::error::ActivityLogStreamError; -use defguard_common::{ - db::{Id, NoId}, - secret::SecretStringWrapper, -}; -#[derive(Debug, Serialize, Deserialize, Type, EnumString, Display, Clone)] +#[derive(Debug, Serialize, Deserialize, Type, EnumString, Display, Clone, PartialEq)] #[sqlx(type_name = "text", rename_all = "snake_case")] #[serde(rename_all = "snake_case")] pub enum ActivityLogStreamType { @@ -19,7 +19,7 @@ pub enum ActivityLogStreamType { LogstashHttp, } -#[derive(Clone, Debug, Serialize, Model, FromRow)] +#[derive(Clone, Debug, Serialize, Model, FromRow, PartialEq)] #[table(activity_log_stream)] pub struct ActivityLogStream { pub id: I, diff --git a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs index 9e01c7248c..7b65a80e96 100644 --- a/crates/defguard_core/src/enterprise/db/models/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/db/models/api_tokens.rs @@ -1,10 +1,9 @@ use chrono::NaiveDateTime; +use defguard_common::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, query_as}; -use defguard_common::db::{Id, NoId}; - -#[derive(Clone, Debug, Deserialize, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, PartialEq)] #[table(api_token)] pub struct ApiToken { pub id: I, diff --git a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs index f2e85ced8b..d1c9be350b 100644 --- a/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs +++ b/crates/defguard_core/src/enterprise/db/models/enterprise_settings.rs @@ -1,4 +1,4 @@ -use sqlx::{PgExecutor, query, query_as}; +use sqlx::{PgExecutor, Type, query, query_as}; use struct_patch::Patch; use crate::enterprise::is_enterprise_enabled; @@ -6,11 +6,11 @@ use crate::enterprise::is_enterprise_enabled; #[derive(Debug, Deserialize, Patch, Serialize)] #[patch(attribute(derive(Deserialize, Serialize)))] pub struct EnterpriseSettings { - // If true, only admins can manage devices + /// If true, only admins can manage devices pub admin_device_management: bool, - // If true, the option to route all traffic through the vpn is disabled in the client - pub disable_all_traffic: bool, - // If true, manual WireGuard setup is disabled + /// Describes allowed routing options for clients connecting to the instance. + pub client_traffic_policy: ClientTrafficPolicy, + /// If true, manual WireGuard setup is disabled pub only_client_activation: bool, } @@ -20,8 +20,8 @@ impl Default for EnterpriseSettings { fn default() -> Self { Self { admin_device_management: false, - disable_all_traffic: false, only_client_activation: false, + client_traffic_policy: ClientTrafficPolicy::default(), } } } @@ -39,7 +39,8 @@ impl EnterpriseSettings { let settings = query_as!( Self, "SELECT admin_device_management, \ - disable_all_traffic, only_client_activation \ + client_traffic_policy \"client_traffic_policy: ClientTrafficPolicy\", \ + only_client_activation \ FROM \"enterprisesettings\" WHERE id = 1", ) .fetch_optional(executor) @@ -57,11 +58,11 @@ impl EnterpriseSettings { query!( "UPDATE \"enterprisesettings\" SET \ admin_device_management = $1, \ - disable_all_traffic = $2, \ + client_traffic_policy = $2, \ only_client_activation = $3 \ WHERE id = 1", self.admin_device_management, - self.disable_all_traffic, + self.client_traffic_policy as ClientTrafficPolicy, self.only_client_activation, ) .execute(executor) @@ -70,3 +71,17 @@ impl EnterpriseSettings { Ok(()) } } + +/// Describes allowed traffic options for clients connecting to the instance. +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, Type, Debug, Default, Copy)] +#[sqlx(type_name = "client_traffic_policy", rename_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum ClientTrafficPolicy { + /// No restrictions + #[default] + None, + /// Clients are not allowed to route all traffic through the VPN. + DisableAllTraffic, + /// Clients are forced to route all traffic through the VPN. + ForceAllTraffic, +} diff --git a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs index 1d6a563268..7a3ef88604 100644 --- a/crates/defguard_core/src/enterprise/db/models/openid_provider.rs +++ b/crates/defguard_core/src/enterprise/db/models/openid_provider.rs @@ -1,10 +1,9 @@ use std::fmt; +use defguard_common::db::{Id, NoId}; use model_derive::Model; use sqlx::{Error as SqlxError, PgExecutor, PgPool, Type, query, query_as}; -use defguard_common::db::{Id, NoId}; - // The behavior when a user is deleted from the directory // Keep: Keep the user, despite being deleted from the external provider's directory // Disable: Disable the user @@ -87,7 +86,7 @@ impl From for DirectorySyncTarget { } } -#[derive(Clone, Debug, Deserialize, Model, Serialize)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, PartialEq)] pub struct OpenIdProvider { pub id: I, pub name: String, @@ -116,6 +115,9 @@ pub struct OpenIdProvider { // The groups to sync from the directory, exact match pub directory_sync_group_match: Vec, pub jumpcloud_api_key: Option, + // Fetch all users from directory and create them in Defguard + // TODO: currently only supported for Microsoft + pub prefetch_users: bool, } impl OpenIdProvider { @@ -138,6 +140,7 @@ impl OpenIdProvider { okta_dirsync_client_id: Option, directory_sync_group_match: Vec, jumpcloud_api_key: Option, + prefetch_users: bool, ) -> Self { Self { id: NoId, @@ -158,6 +161,7 @@ impl OpenIdProvider { okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, + prefetch_users, } } @@ -170,8 +174,9 @@ impl OpenIdProvider { directory_sync_interval = $10, directory_sync_user_behavior = $11, \ directory_sync_admin_behavior = $12, directory_sync_target = $13, \ okta_private_jwk = $14, okta_dirsync_client_id = $15, \ - directory_sync_group_match = $16, jumpcloud_api_key = $17 \ - WHERE id = $18", + directory_sync_group_match = $16, jumpcloud_api_key = $17, \ + prefetch_users = $18 \ + WHERE id = $19", self.name, self.base_url, self.client_id, @@ -189,6 +194,7 @@ impl OpenIdProvider { self.okta_dirsync_client_id, &self.directory_sync_group_match, self.jumpcloud_api_key, + self.prefetch_users, provider.id, ) .execute(pool) @@ -216,7 +222,7 @@ impl OpenIdProvider { directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", \ directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", \ directory_sync_target \"directory_sync_target: DirectorySyncTarget\", \ - okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users \ FROM openidprovider WHERE name = $1", name ) @@ -235,7 +241,7 @@ impl OpenIdProvider { directory_sync_interval, directory_sync_user_behavior \"directory_sync_user_behavior: DirectorySyncUserBehavior\", \ directory_sync_admin_behavior \"directory_sync_admin_behavior: DirectorySyncUserBehavior\", \ directory_sync_target \"directory_sync_target: DirectorySyncTarget\", \ - okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key \ + okta_private_jwk, okta_dirsync_client_id, directory_sync_group_match, jumpcloud_api_key, prefetch_users \ FROM openidprovider LIMIT 1" ) .fetch_optional(executor) diff --git a/crates/defguard_core/src/enterprise/db/models/snat.rs b/crates/defguard_core/src/enterprise/db/models/snat.rs index 68b7b71c56..c4ae69fc8e 100644 --- a/crates/defguard_core/src/enterprise/db/models/snat.rs +++ b/crates/defguard_core/src/enterprise/db/models/snat.rs @@ -1,14 +1,14 @@ use std::net::IpAddr; +use defguard_common::db::{Id, NoId}; use model_derive::Model; use serde::{Deserialize, Serialize}; use sqlx::{PgExecutor, query_as}; use utoipa::ToSchema; use crate::enterprise::snat::error::UserSnatBindingError; -use defguard_common::db::{Id, NoId}; -#[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, Model, Serialize, ToSchema, PartialEq)] #[table(user_snat_binding)] pub struct UserSnatBinding { pub id: I, diff --git a/crates/defguard_core/src/enterprise/directory_sync/google.rs b/crates/defguard_core/src/enterprise/directory_sync/google.rs index 4f7a5139fa..642af28e62 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/google.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/google.rs @@ -106,6 +106,8 @@ impl From for DirectoryUser { email: val.primary_email, active: !val.suspended, id: None, + // TODO: currently not supported for Google + user_details: None, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs index aad95011ae..93d8f78280 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/jumpcloud.rs @@ -39,6 +39,8 @@ impl From for DirectoryUser { email: user.email, active: user.activated && !user.account_locked && user.state == UserState::Activated, id: Some(user.id), + // TODO: currently not supported for Jumpcloud + user_details: None, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs index 48ebef13a5..7e02645e7f 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/microsoft.rs @@ -6,7 +6,7 @@ use super::{ DirectoryGroup, DirectorySync, DirectorySyncError, DirectoryUser, REQUEST_PAGINATION_SLOWDOWN, make_get_request, parse_response, }; -use crate::enterprise::directory_sync::REQUEST_TIMEOUT; +use crate::enterprise::directory_sync::{DirectoryUserDetails, REQUEST_TIMEOUT}; pub(crate) struct MicrosoftDirectorySync { access_token: Option, @@ -26,7 +26,8 @@ const MICROSOFT_DEFAULT_SCOPE: &str = "https://graph.microsoft.com/.default"; const GRANT_TYPE: &str = "client_credentials"; const MAX_RESULTS: &str = "200"; const MAX_REQUESTS: usize = 50; -const USER_QUERY_FIELDS: &str = "accountEnabled,displayName,mail,otherMails"; +const USER_QUERY_FIELDS: &str = + "accountEnabled,displayName,mail,otherMails,id,givenName,surname,mobilePhone,businessPhones"; const USER_SEARCH_URL: &str = "https://graph.microsoft.com/v1.0/users?$select=id&$filter=mail eq '{email}'"; const USER_SEARCH_URL_FALLBACK: &str = @@ -103,6 +104,7 @@ impl From for Vec { #[derive(Debug, Serialize, Deserialize)] struct User { + id: String, #[serde(rename = "displayName")] display_name: String, mail: Option, @@ -110,6 +112,13 @@ struct User { account_enabled: bool, #[serde(rename = "otherMails")] other_mails: Vec, + #[serde(rename = "givenName")] + given_name: Option, + surname: Option, + #[serde(rename = "mobilePhone")] + mobile_phone: Option, + #[serde(rename = "businessPhones")] + business_phones: Vec, } #[derive(Debug, Serialize, Deserialize, Default)] @@ -125,11 +134,26 @@ impl From for Vec { .value .into_iter() .filter_map(|user| { +// check if additional user detail data is available +let user_details = if let ( Some(first_name), Some(last_name)) = ( user.given_name, user.surname) { + // get a phone number if any is available + // prefer mobile phone + let phone_number = match user.mobile_phone { + Some(mobile_phone) => Some(mobile_phone), + None => user.business_phones.into_iter().next() + }; + Some(DirectoryUserDetails { last_name, first_name, phone_number }) +} else { + debug!("User {} doesn't have all required user details and will be skipped if user creation is required", user.display_name); + None +}; + + if let Some(email) = user.mail { - Some(DirectoryUser { email, active: user.account_enabled, id: None }) + Some(DirectoryUser { email, active: user.account_enabled, id: Some(user.id), user_details }) } else if let Some(email) = user.other_mails.into_iter().next() { warn!("User {} doesn't have a primary email address set, his first additional email address will be used: {email}", user.display_name); - Some(DirectoryUser { email, active: user.account_enabled, id: None }) + Some(DirectoryUser { email, active: user.account_enabled, id: Some(user.id), user_details }) } else { warn!("User {} doesn't have any email address and will be skipped in synchronization.", user.display_name); None @@ -621,18 +645,33 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + given_name: Some("User".into()), + surname: Some("One".into()), + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, User { display_name: "User 2".to_string(), mail: None, account_enabled: true, other_mails: vec!["email2@email.com".to_string()], + id: "user2-id".into(), + given_name: Some("User".into()), + surname: Some("Two".into()), + mobile_phone: None, + business_phones: vec![], }, User { display_name: "User 3".to_string(), mail: None, account_enabled: true, other_mails: vec![], + id: "user3-id".into(), + given_name: Some("User".into()), + surname: Some("Three".into()), + mobile_phone: None, + business_phones: vec![], }, ], }; @@ -653,18 +692,33 @@ mod tests { mail: Some("email@email.com".to_string()), account_enabled: true, other_mails: vec![], + id: "user1-id".into(), + given_name: Some("User".into()), + surname: None, + mobile_phone: None, + business_phones: vec![], }, User { display_name: "User 2".to_string(), mail: None, account_enabled: true, other_mails: vec!["email2@email.com".to_string()], + id: "user2-id".into(), + given_name: None, + surname: None, + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, User { display_name: "User 3".to_string(), mail: None, account_enabled: true, other_mails: vec![], + id: "user3-id".into(), + given_name: Some("User".into()), + surname: Some("Three".into()), + mobile_phone: Some("555555555".into()), + business_phones: vec![], }, ], }; diff --git a/crates/defguard_core/src/enterprise/directory_sync/mod.rs b/crates/defguard_core/src/enterprise/directory_sync/mod.rs index 70ddd638ce..b37fccba56 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/mod.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/mod.rs @@ -1,12 +1,13 @@ use std::{ collections::{HashMap, HashSet}, + fmt::Debug, time::Duration, }; -use defguard_common::db::Id; +use defguard_common::db::{Id, models::Settings}; use paste::paste; use reqwest::header::AUTHORIZATION; -use sqlx::{PgPool, error::Error as SqlxError}; +use sqlx::{PgConnection, PgPool, error::Error as SqlxError}; use thiserror::Error; use tokio::sync::broadcast::Sender; @@ -20,8 +21,10 @@ use crate::{ db::{GatewayEvent, Group, User}, enterprise::{ db::models::openid_provider::DirectorySyncUserBehavior, + handlers::openid_login::prune_username, ldap::utils::{ldap_add_users_to_groups, ldap_delete_users, ldap_remove_users_from_groups}, }, + handlers::user::check_username, }; const REQUEST_TIMEOUT: Duration = Duration::from_secs(10); @@ -55,6 +58,8 @@ pub enum DirectorySyncError { NetworkUpdateError(String), #[error("Failed to update user state: {0}")] UserUpdateError(String), + #[error("Failed to create user: {0}")] + UserCreateError(String), #[error("Failed to find user: {0}")] UserNotFound(String), #[error( @@ -100,6 +105,16 @@ pub struct DirectoryUser { pub email: String, // Users may be disabled/suspended in the directory pub active: bool, + // Currently only supported for Microsoft Entra + user_details: Option, +} + +// additional user details required for user creation +#[derive(Debug, Serialize, Deserialize)] +pub struct DirectoryUserDetails { + last_name: String, + first_name: String, + phone_number: Option, } #[trait_variant::make(Send)] @@ -594,71 +609,127 @@ async fn sync_all_users_state( .await? .ok_or(DirectorySyncError::NotConfigured)?; + // prepare relevant settings let user_behavior = settings.directory_sync_user_behavior; let admin_behavior = settings.directory_sync_admin_behavior; + let prefetch_users = settings.prefetch_users; - let emails = all_users - .iter() - .map(|u| u.email.as_str()) - .collect::>(); - let missing_users = User::exclude(&mut *transaction, &emails) - .await? - .into_iter() - .collect::>>(); + // split directory users into separate lists for active and inactive users + let (active_directory_users, inactive_directory_users): (Vec<_>, Vec<_>) = + all_users.iter().partition(|user| user.active); - let disabled_users_emails = all_users + // prepare a list of user emails for matching users between directory and Defguard + let all_directory_emails = all_users .iter() - .filter(|u| !u.active) .map(|u| u.email.as_str()) .collect::>(); - let users_to_disable = - User::find_many_by_emails(&mut *transaction, &disabled_users_emails).await?; - - let enabled_users_emails = all_users - .iter() - .filter(|u| u.active) - .map(|u| u.email.as_str()) - .collect::>(); - let users_to_enable = - User::find_many_by_emails(&mut *transaction, &enabled_users_emails).await?; - - debug!( - "There are {} disabled users in the directory, disabling them in Defguard...", - users_to_disable.len() - ); + // setup Vecs for tracking user updates let mut modified_users = Vec::new(); let mut deleted_users = Vec::new(); + let mut created_users = Vec::new(); - for mut user in users_to_disable { - if user.is_active { - debug!( - "Disabling user {} because they are disabled in the directory", - user.email - ); - user.disable(&mut transaction, wg_tx).await.map_err(|err| { - DirectorySyncError::UserUpdateError(format!( - "Failed to disable user {} during directory synchronization: {err}", - user.email - )) - })?; - modified_users.push(user); - } else { - debug!("User {} is already disabled, skipping", user.email); + sync_inactive_directory_users( + &mut transaction, + &inactive_directory_users, + &mut modified_users, + wg_tx, + ) + .await?; + + sync_active_directory_users( + &mut transaction, + &active_directory_users, + &mut modified_users, + ) + .await?; + + // TODO: prefetching users is currently only supported for Microsoft Entra + if prefetch_users && ["Microsoft", "Test"].contains(&settings.name.as_str()) { + // get emails of all directory users who already exist in Defguard + let existing_users = + User::find_many_by_emails(&mut *transaction, &all_directory_emails).await?; + let existing_user_emails: Vec<&str> = existing_users + .iter() + .map(|user| user.email.as_str()) + .collect(); + + // find all directory users not present in Defguard + let missing_defguard_users: Vec<_> = all_users + .iter() + .filter(|user| !existing_user_emails.contains(&user.email.as_str())) + .collect(); + + let core_settings = Settings::get_current_settings(); + + // create missing users + for directory_user in missing_defguard_users { + match &directory_user.user_details { + None => { + error!( + "Missing directory user details for user {directory_user:?}. Unable to create missing Defguard user." + ); + } + Some(details) => { + debug!( + "User {directory_user:?} exists in directory but not in Defguard. Creating new Defguard user.", + ); + + // Extract the username from the email address + let email = directory_user.email.clone(); + let username = + email + .split('@') + .next() + .ok_or(DirectorySyncError::UserCreateError(format!( + "Failed to extract username from email address {email}" + )))?; + let username = prune_username(username, core_settings.openid_username_handling); + check_username(&username).map_err(|err| { + DirectorySyncError::UserCreateError(format!( + "Username {username} validation failed: {err:?}" + )) + })?; + + // Check if user with the same username already exists (usernames are unique). + if User::find_by_username(pool, &username).await?.is_some() { + return Err(DirectorySyncError::UserCreateError(format!( + "User with username {username} already exists" + ))); + } + + let mut user = User::new( + username, + None, + details.last_name.clone(), + details.first_name.clone(), + directory_user.email.clone(), + details.phone_number.clone(), + ); + user.openid_sub = directory_user.id.clone(); + let new_user = user.save(&mut *transaction).await?; + created_users.push(new_user); + } + } } } - debug!("Done processing disabled users"); + + // get all users present in Defguard but not in directory + let missing_directory_users = User::exclude(&mut *transaction, &all_directory_emails) + .await? + .into_iter() + .collect::>>(); debug!( "There are {} users missing from the directory but present in Defguard, \ deciding what to do next based on the following settings: user action: {}, admin action: {}", - missing_users.len(), + missing_directory_users.len(), user_behavior, admin_behavior ); // Keep the admin count to prevent deleting the last admin let mut admin_count = User::find_admins(&mut *transaction).await?.len(); - for mut user in missing_users { + for mut user in missing_directory_users { if user.is_admin(&mut *transaction).await? { match admin_behavior { DirectorySyncUserBehavior::Keep => { @@ -770,8 +841,90 @@ async fn sync_all_users_state( } debug!("Done processing missing users"); + transaction.commit().await?; + + // trigger LDAP sync + ldap_delete_users(deleted_users.iter().collect::>(), pool).await; + Box::pin(ldap_update_users_state( + modified_users.iter_mut().collect::>(), + pool, + )) + .await; + Box::pin(ldap_update_users_state( + created_users.iter_mut().collect::>(), + pool, + )) + .await; + + info!("Syncing all users' state with the directory done"); + + Ok(()) +} + +async fn sync_inactive_directory_users( + transaction: &mut PgConnection, + inactive_directory_users: &[&DirectoryUser], + modified_users: &mut Vec>, + wg_tx: &Sender, +) -> Result<(), DirectorySyncError> { + // find all active Defguard users disabled in directory + let disabled_users_emails = inactive_directory_users + .iter() + .map(|u| u.email.as_str()) + .collect::>(); + let users_to_disable: Vec> = + User::find_many_by_emails(&mut *transaction, &disabled_users_emails) + .await? + .into_iter() + .filter(|user| user.is_active) + .collect(); + + debug!( + "There are {} active Defguard users disabled in the directory. Disabling them in Defguard...", + users_to_disable.len() + ); + + for mut user in users_to_disable { + if user.is_active { + debug!( + "Disabling user {} because they are disabled in the directory", + user.email + ); + user.disable(transaction, wg_tx).await.map_err(|err| { + DirectorySyncError::UserUpdateError(format!( + "Failed to disable user {} during directory synchronization: {err}", + user.email + )) + })?; + modified_users.push(user); + } else { + debug!("User {} is already disabled, skipping", user.email); + } + } + debug!("Done processing disabled directory users"); + + Ok(()) +} + +async fn sync_active_directory_users( + transaction: &mut PgConnection, + active_directory_users: &[&DirectoryUser], + modified_users: &mut Vec>, +) -> Result<(), DirectorySyncError> { + // find all inactive Defguard users enabled in directory + let enabled_users_emails = active_directory_users + .iter() + .map(|u| u.email.as_str()) + .collect::>(); + let users_to_enable: Vec> = + User::find_many_by_emails(&mut *transaction, &enabled_users_emails) + .await? + .into_iter() + .filter(|user| !user.is_active) + .collect(); + debug!( - "There are {} enabled users in the directory, enabling them in Defguard if they were previously disabled", + "There are {} inactive Defguard users enabled in the directory. Enabling them in Defguard...", users_to_enable.len() ); for mut user in users_to_enable { @@ -787,17 +940,7 @@ async fn sync_all_users_state( user.save(&mut *transaction).await?; modified_users.push(user); } - debug!("Done processing enabled users"); - transaction.commit().await?; - - ldap_delete_users(deleted_users.iter().collect::>(), pool).await; - Box::pin(ldap_update_users_state( - modified_users.iter_mut().collect::>(), - pool, - )) - .await; - - info!("Syncing all users' state with the directory done"); + debug!("Done processing active directory users"); Ok(()) } diff --git a/crates/defguard_core/src/enterprise/directory_sync/okta.rs b/crates/defguard_core/src/enterprise/directory_sync/okta.rs index bbc168fe16..569f4ee473 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/okta.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/okta.rs @@ -96,6 +96,8 @@ impl From for DirectoryUser { email: val.profile.email, active: ACTIVE_STATUS.contains(&val.status.as_str()), id: None, + // TODO: currently not supported for Okta + user_details: None, } } } diff --git a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs index fc74bdebfd..b73d5abbed 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/testprovider.rs @@ -53,16 +53,31 @@ impl DirectorySync for TestProviderDirectorySync { email: "testuser@email.com".into(), active: true, id: Some("testuser-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "User".into(), + first_name: "Test".into(), + phone_number: None, + }), }, DirectoryUser { email: "testuserdisabled@email.com".into(), active: false, id: Some("testuserdisabled-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "UserDisabled".into(), + first_name: "Test".into(), + phone_number: None, + }), }, DirectoryUser { email: "testuser2@email.com".into(), active: true, id: Some("testuser2-id".into()), + user_details: Some(crate::enterprise::directory_sync::DirectoryUserDetails { + last_name: "User2".into(), + first_name: "Test".into(), + phone_number: None, + }), }, ]) } diff --git a/crates/defguard_core/src/enterprise/directory_sync/tests.rs b/crates/defguard_core/src/enterprise/directory_sync/tests.rs index d6ae4f4beb..9bac17d281 100644 --- a/crates/defguard_core/src/enterprise/directory_sync/tests.rs +++ b/crates/defguard_core/src/enterprise/directory_sync/tests.rs @@ -18,7 +18,10 @@ mod test { use crate::{ db::{ Device, Session, SessionState, WireguardNetwork, - models::{device::DeviceType, wireguard::LocationMfaMode}, + models::{ + device::DeviceType, + wireguard::{LocationMfaMode, ServiceLocationMode}, + }, }, enterprise::db::models::openid_provider::DirectorySyncTarget, }; @@ -37,6 +40,7 @@ mod test { user_behavior: DirectorySyncUserBehavior, admin_behavior: DirectorySyncUserBehavior, target: DirectorySyncTarget, + prefetch_users: bool, ) -> OpenIdProvider { Settings::init_defaults(pool).await.unwrap(); initialize_current_settings(pool).await.unwrap(); @@ -59,6 +63,7 @@ mod test { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(pool) .await @@ -82,6 +87,7 @@ mod test { None, vec![], None, + prefetch_users, ) .save(pool) .await @@ -142,6 +148,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -181,6 +188,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -227,6 +235,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -279,6 +288,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; User::init_admin_user(&pool, config.default_admin_password.expose_secret()) @@ -349,6 +359,7 @@ mod test { DirectorySyncUserBehavior::Disable, DirectorySyncUserBehavior::Keep, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -431,6 +442,7 @@ mod test { DirectorySyncUserBehavior::Keep, DirectorySyncUserBehavior::Disable, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -508,6 +520,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -564,6 +577,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -592,6 +606,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::Users, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -616,6 +631,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let network = get_test_network(&pool).await; @@ -671,6 +687,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::Groups, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -698,6 +715,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -744,6 +762,7 @@ mod test { DirectorySyncUserBehavior::Delete, DirectorySyncUserBehavior::Delete, DirectorySyncTarget::All, + false, ) .await; let mut client = DirectorySyncClient::build(&pool).await.unwrap(); @@ -770,4 +789,72 @@ mod test { let user = User::find_by_username(&pool, "defguard").await.unwrap(); assert!(user.is_none()); } + + #[sqlx::test] + async fn test_users_no_prefetch(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let config = DefGuardConfig::new_test_config(); + let _ = SERVER_CONFIG.set(config.clone()); + let (wg_tx, mut wg_rx) = broadcast::channel::(16); + + // disable prefetching users + make_test_provider( + &pool, + DirectorySyncUserBehavior::Keep, + DirectorySyncUserBehavior::Keep, + DirectorySyncTarget::All, + false, + ) + .await; + let mut client = DirectorySyncClient::build(&pool).await.unwrap(); + client.prepare().await.unwrap(); + + // no users in Defguard before sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + do_directory_sync(&pool, &wg_tx).await.unwrap(); + + // no users in Defguard after sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + // No events + assert!(wg_rx.try_recv().is_err()); + } + + #[sqlx::test] + async fn test_users_prefetch(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let config = DefGuardConfig::new_test_config(); + let _ = SERVER_CONFIG.set(config.clone()); + let (wg_tx, mut wg_rx) = broadcast::channel::(16); + + // enable prefetching users + make_test_provider( + &pool, + DirectorySyncUserBehavior::Keep, + DirectorySyncUserBehavior::Keep, + DirectorySyncTarget::All, + true, + ) + .await; + let mut client = DirectorySyncClient::build(&pool).await.unwrap(); + client.prepare().await.unwrap(); + + // no users in Defguard before sync + let defguard_users = User::all(&pool).await.unwrap(); + assert!(defguard_users.is_empty()); + + do_directory_sync(&pool, &wg_tx).await.unwrap(); + + // all active directory users were synced + let defguard_users = User::all(&pool).await.unwrap(); + assert_eq!(defguard_users.len(), 3); + + // No events + assert!(wg_rx.try_recv().is_err()); + } } diff --git a/crates/defguard_core/src/enterprise/firewall/mod.rs b/crates/defguard_core/src/enterprise/firewall/mod.rs index a87f9110ac..5e2b7e8d97 100644 --- a/crates/defguard_core/src/enterprise/firewall/mod.rs +++ b/crates/defguard_core/src/enterprise/firewall/mod.rs @@ -4,6 +4,11 @@ use std::{ }; use defguard_common::db::{Id, models::ModelError}; +use defguard_proto::enterprise::firewall::{ + FirewallConfig, FirewallPolicy, FirewallRule, IpAddress, IpRange, IpVersion, Port, + PortRange as PortRangeProto, SnatBinding as SnatBindingProto, ip_address::Address, + port::Port as PortInner, +}; use ipnetwork::IpNetwork; use sqlx::{Error as SqlxError, PgConnection, query_as, query_scalar}; @@ -21,11 +26,6 @@ use crate::{ is_enterprise_enabled, }, }; -use defguard_proto::enterprise::firewall::{ - FirewallConfig, FirewallPolicy, FirewallRule, IpAddress, IpRange, IpVersion, Port, - PortRange as PortRangeProto, SnatBinding as SnatBindingProto, ip_address::Address, - port::Port as PortInner, -}; #[derive(Debug, thiserror::Error)] pub enum FirewallError { diff --git a/crates/defguard_core/src/enterprise/firewall/tests.rs b/crates/defguard_core/src/enterprise/firewall/tests.rs index 0785fb85fc..abad1a6910 100644 --- a/crates/defguard_core/src/enterprise/firewall/tests.rs +++ b/crates/defguard_core/src/enterprise/firewall/tests.rs @@ -2,6 +2,10 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use chrono::{DateTime, NaiveDateTime}; use defguard_common::db::{Id, NoId, setup_pool}; +use defguard_proto::enterprise::firewall::{ + FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, + ip_address::Address, port::Port as PortInner, +}; use ipnetwork::{IpNetwork, Ipv6Network}; use rand::{Rng, thread_rng}; use sqlx::{ @@ -27,10 +31,6 @@ use crate::{ firewall::{get_source_addrs, get_source_network_devices}, }, }; -use defguard_proto::enterprise::firewall::{ - FirewallPolicy, IpAddress, IpRange, IpVersion, Port, PortRange as PortRangeProto, Protocol, - ip_address::Address, port::Port as PortInner, -}; impl Default for AclRuleDestinationRange { fn default() -> Self { diff --git a/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs index eeb022224f..6e5e8d032d 100644 --- a/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs +++ b/crates/defguard_core/src/enterprise/grpc/desktop_client_mfa.rs @@ -1,3 +1,4 @@ +use defguard_proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod}; use openidconnect::{AuthorizationCode, Nonce}; use reqwest::Url; use tonic::Status; @@ -10,10 +11,9 @@ use crate::{ events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, grpc::{ client_mfa::{ClientLoginSession, ClientMfaServer}, - utils::parse_client_info, + utils::parse_client_ip_agent, }, }; -use defguard_proto::proxy::{ClientMfaOidcAuthenticateRequest, DeviceInfo, MfaMethod}; impl ClientMfaServer { #[instrument(skip_all)] @@ -66,7 +66,7 @@ impl ClientMfaServer { return Err(Status::invalid_argument("invalid MFA method")); } - let (ip, _user_agent) = parse_client_info(&info).map_err(Status::internal)?; + let (ip, _user_agent) = parse_client_ip_agent(&info).map_err(Status::internal)?; let context = BidiRequestContext::new( user.id, user.username.clone(), diff --git a/crates/defguard_core/src/enterprise/grpc/polling.rs b/crates/defguard_core/src/enterprise/grpc/polling.rs index 782ca70fab..8e04e9a411 100644 --- a/crates/defguard_core/src/enterprise/grpc/polling.rs +++ b/crates/defguard_core/src/enterprise/grpc/polling.rs @@ -1,4 +1,5 @@ use defguard_common::db::Id; +use defguard_proto::proxy::{DeviceInfo, InstanceInfoRequest, InstanceInfoResponse}; use sqlx::PgPool; use tonic::Status; @@ -7,7 +8,6 @@ use crate::{ enterprise::is_enterprise_enabled, grpc::utils::build_device_config_response, }; -use defguard_proto::proxy::{InstanceInfoRequest, InstanceInfoResponse}; pub struct PollingServer { pool: PgPool, @@ -47,7 +47,11 @@ impl PollingServer { /// Prepares instance info for polling requests. Enterprise only. #[instrument(skip_all)] - pub async fn info(&self, request: InstanceInfoRequest) -> Result { + pub async fn info( + &self, + request: InstanceInfoRequest, + device_info: Option, + ) -> Result { trace!("Polling info start"); let token = self.validate_session(&request.token).await?; let Some(device) = Device::find_by_id(&self.pool, token.device_id) @@ -82,7 +86,8 @@ impl PollingServer { } // Build and return polling info. - let device_config = build_device_config_response(&self.pool, device, None).await?; + let device_config = + build_device_config_response(&self.pool, device, None, device_info).await?; Ok(InstanceInfoResponse { device_config: Some(device_config), diff --git a/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs b/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs index f125fddf5e..e8bbe7573a 100644 --- a/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs +++ b/crates/defguard_core/src/enterprise/handlers/activity_log_stream.rs @@ -2,6 +2,7 @@ use axum::{ Json, extract::{Path, State}, }; +use defguard_common::db::{Id, NoId}; use reqwest::StatusCode; use serde_json::json; @@ -15,7 +16,6 @@ use crate::{ events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult}, }; -use defguard_common::db::{Id, NoId}; pub async fn get_activity_log_stream( _admin: AdminRole, diff --git a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs index 12132fbc7e..15a40ecfda 100644 --- a/crates/defguard_core/src/enterprise/handlers/api_tokens.rs +++ b/crates/defguard_core/src/enterprise/handlers/api_tokens.rs @@ -4,6 +4,7 @@ use axum::{ http::StatusCode, }; use chrono::Utc; +use defguard_common::random::gen_alphanumeric; use serde_json::json; use super::LicenseInfo; @@ -16,7 +17,6 @@ use crate::{ events::{ApiEvent, ApiEventType, ApiRequestContext}, handlers::{ApiResponse, ApiResult, user_for_admin_or_self}, }; -use defguard_common::random::gen_alphanumeric; const API_TOKEN_LENGTH: usize = 32; diff --git a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs index 565e573913..01cef376e6 100644 --- a/crates/defguard_core/src/enterprise/handlers/openid_providers.rs +++ b/crates/defguard_core/src/enterprise/handlers/openid_providers.rs @@ -43,6 +43,7 @@ pub struct AddProviderData { pub directory_sync_group_match: Option, pub username_handling: OpenidUsernameHandling, pub jumpcloud_api_key: Option, + pub prefetch_users: bool, } #[derive(Deserialize, Serialize)] @@ -160,6 +161,7 @@ pub async fn add_openid_provider( provider_data.okta_dirsync_client_id, group_match, provider_data.jumpcloud_api_key, + provider_data.prefetch_users, ) .upsert(&appstate.pool) .await?; diff --git a/crates/defguard_core/src/enterprise/ldap/client.rs b/crates/defguard_core/src/enterprise/ldap/client.rs index 36a5f1b0d9..0524f3b649 100644 --- a/crates/defguard_core/src/enterprise/ldap/client.rs +++ b/crates/defguard_core/src/enterprise/ldap/client.rs @@ -4,6 +4,7 @@ use std::{ time::Duration, }; +use defguard_common::db::models::Settings; use ldap3::{ LdapConnAsync, LdapConnSettings, Mod, Scope, SearchEntry, adapters::PagedResults, drive, ldap_escape, @@ -11,7 +12,6 @@ use ldap3::{ use super::error::LdapError; use crate::{db::User, enterprise::ldap::model::extract_rdn_value}; -use defguard_common::db::models::Settings; impl super::LDAPConnection { pub(crate) async fn create() -> Result { diff --git a/crates/defguard_core/src/enterprise/ldap/hash.rs b/crates/defguard_core/src/enterprise/ldap/hash.rs index ddb6d4c8d6..86aec6fa7a 100644 --- a/crates/defguard_core/src/enterprise/ldap/hash.rs +++ b/crates/defguard_core/src/enterprise/ldap/hash.rs @@ -2,6 +2,7 @@ use base64::Engine; use defguard_common::hex::to_lower_hex; use md4::Md4; use rand::{RngCore, rngs::OsRng}; +#[allow(deprecated)] use sha1::{ Digest, Sha1, digest::generic_array::{GenericArray, sequence::Concat}, @@ -18,6 +19,7 @@ pub fn salted_sha1_hash(password: &str) -> String { pass.extend_from_slice(&salt); let checksum = Sha1::digest(pass); + #[allow(deprecated)] let checksum = checksum.concat(GenericArray::from(salt)); format!( diff --git a/crates/defguard_core/src/enterprise/ldap/model.rs b/crates/defguard_core/src/enterprise/ldap/model.rs index 79a295732d..0da8afc1c7 100644 --- a/crates/defguard_core/src/enterprise/ldap/model.rs +++ b/crates/defguard_core/src/enterprise/ldap/model.rs @@ -284,7 +284,7 @@ impl User { SELECT id, username, password_hash, last_name, first_name, email, phone, \ mfa_enabled, totp_enabled, email_mfa_enabled, totp_secret, email_mfa_secret, \ mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE ldap_user_path IS NULL ", ) diff --git a/crates/defguard_core/src/enterprise/license.rs b/crates/defguard_core/src/enterprise/license.rs index 499839d8cd..2bec3c4747 100644 --- a/crates/defguard_core/src/enterprise/license.rs +++ b/crates/defguard_core/src/enterprise/license.rs @@ -3,6 +3,12 @@ use std::time::Duration; use anyhow::Result; use base64::prelude::*; use chrono::{DateTime, TimeDelta, Utc}; +use defguard_common::{ + VERSION, + config::server_config, + db::models::{Settings, settings::update_current_settings}, + global_value, +}; use humantime::format_duration; use pgp::{ composed::{Deserializable, SignedPublicKey, StandaloneSignature}, @@ -15,12 +21,6 @@ use tokio::time::sleep; use super::limits::Counts; use crate::grpc::proto::enterprise::license::{LicenseKey, LicenseLimits, LicenseMetadata}; -use defguard_common::{ - VERSION, - config::server_config, - db::models::{Settings, settings::update_current_settings}, - global_value, -}; const LICENSE_SERVER_URL: &str = "https://pkgs.defguard.net/api/license/renew"; diff --git a/crates/defguard_core/src/events.rs b/crates/defguard_core/src/events.rs index 68bc260a72..dc316c0992 100644 --- a/crates/defguard_core/src/events.rs +++ b/crates/defguard_core/src/events.rs @@ -5,6 +5,7 @@ use defguard_common::db::{ Id, models::{AuthenticationKey, MFAMethod, Settings}, }; +use defguard_proto::proxy::MfaMethod; use crate::{ db::{ @@ -16,14 +17,13 @@ use crate::{ openid_provider::OpenIdProvider, snat::UserSnatBinding, }, }; -use defguard_proto::proxy::MfaMethod; /// Shared context that needs to be added to every API event /// /// Mainly meant to be stored in the activity log. /// By design this is a duplicate of a similar struct in the `event_logger` module. /// This is done in order to avoid circular imports once we split the project into multiple crates. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ApiRequestContext { pub timestamp: NaiveDateTime, pub user_id: Id, @@ -83,7 +83,7 @@ impl GrpcRequestContext { } } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum ApiEventType { UserLogin, UserLoginFailed { diff --git a/crates/defguard_core/src/grpc/client_mfa.rs b/crates/defguard_core/src/grpc/client_mfa.rs index e2b86685b0..f688a41a48 100644 --- a/crates/defguard_core/src/grpc/client_mfa.rs +++ b/crates/defguard_core/src/grpc/client_mfa.rs @@ -9,6 +9,11 @@ use defguard_common::{ }, }; use defguard_mail::Mail; +use defguard_proto::proxy::{ + self, ClientMfaFinishRequest, ClientMfaFinishResponse, ClientMfaStartRequest, + ClientMfaStartResponse, ClientMfaTokenValidationRequest, ClientMfaTokenValidationResponse, + MfaMethod, +}; use sqlx::PgPool; use thiserror::Error; use tokio::sync::{ @@ -27,14 +32,9 @@ use crate::{ }, enterprise::{db::models::openid_provider::OpenIdProvider, is_enterprise_enabled}, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, DesktopClientMfaEvent}, - grpc::utils::parse_client_info, + grpc::utils::parse_client_ip_agent, handlers::mail::send_email_mfa_code_email, }; -use defguard_proto::proxy::{ - self, ClientMfaFinishRequest, ClientMfaFinishResponse, ClientMfaStartRequest, - ClientMfaStartResponse, ClientMfaTokenValidationRequest, ClientMfaTokenValidationResponse, - MfaMethod, -}; const CLIENT_SESSION_TIMEOUT: u64 = 60 * 5; // 10 minutes @@ -395,7 +395,7 @@ impl ClientMfaServer { } = session; // Prepare event context - let (ip, _user_agent) = parse_client_info(&info).map_err(Status::internal)?; + let (ip, _user_agent) = parse_client_ip_agent(&info).map_err(Status::internal)?; let context = BidiRequestContext::new( user.id, user.username.clone(), diff --git a/crates/defguard_core/src/grpc/client_version.rs b/crates/defguard_core/src/grpc/client_version.rs new file mode 100644 index 0000000000..d8bc7be95e --- /dev/null +++ b/crates/defguard_core/src/grpc/client_version.rs @@ -0,0 +1,386 @@ +use base64::{Engine, prelude::BASE64_STANDARD}; +use defguard_proto::proxy::{ClientPlatformInfo, DeviceInfo}; +use prost::Message; +use semver::Version; + +pub(crate) fn parse_client_version_platform( + info: Option<&DeviceInfo>, +) -> (Option, Option) { + let Some(info) = info else { + debug!("Device information is missing from the request"); + return (None, None); + }; + + let version = info.version.as_ref().map_or_else( + || None, + |v| { + Version::parse(v).map_or_else( + |_| { + error!("Invalid version string: {v}"); + None + }, + Some, + ) + }, + ); + + let platform = info.platform.as_ref().and_then(|p| { + let binary = BASE64_STANDARD + .decode(p) + .map_err(|e| { + error!("Failed to decode base64 platform string: {e}"); + e + }) + .ok()?; + let platform_info = ClientPlatformInfo::decode(&*binary) + .map_err(|e| { + error!("Failed to decode ClientPlatformInfo from bytes: {e}"); + e + }) + .ok()?; + Some(platform_info) + }); + + (version, platform) +} + +/// Represents a client feature that may have minimum version and OS family requirements. +#[derive(Debug)] +pub(crate) enum ClientFeature { + ServiceLocations, +} + +impl ClientFeature { + const fn min_version(&self) -> Option { + match self { + Self::ServiceLocations => Some(Version::new(1, 6, 0)), + } + } + + fn required_os_family(&self) -> Option> { + match self { + Self::ServiceLocations => Some(vec!["windows"]), + } + } + + pub(crate) fn is_supported_by_device(&self, info: Option<&DeviceInfo>) -> bool { + let (version, platform) = parse_client_version_platform(info); + + // No minimum version = matches all + let version_matches = self.min_version().is_none_or(|min_version| { + // No version info = does not match + version + .as_ref() + .is_some_and(|version| version >= &min_version) + }); + + if !version_matches { + debug!( + "Client version {version:?} does not meet minimum version {:?} for feature {self:?}", + self.min_version() + ); + } + + // No required OS family = matches all + let platform_matches = self.required_os_family().is_none_or(|platforms| { + platforms.iter().any(|p| { + platform + .as_ref() + .is_some_and(|platform| platform.os_family.eq_ignore_ascii_case(p)) + }) + }); + + if !platform_matches { + debug!( + "Client OS {:?} does not meet required OS {:?} for feature {self:?}", + platform.as_ref().map(|p| &p.os_family), + self.required_os_family() + ); + } + + version_matches && platform_matches + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Helper function to create DeviceInfo + fn create_device_info( + version: Option, + platform: Option, + ) -> DeviceInfo { + let platform = platform.map(|p| { + let mut buf = Vec::new(); + p.encode(&mut buf).unwrap(); + BASE64_STANDARD.encode(&buf) + }); + + DeviceInfo { + version, + platform, + ..Default::default() + } + } + + #[test] + fn test_parse_client_version_platform() { + // Test with valid version and platform + let info = create_device_info( + Some("1.5.0".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_some()); + assert_eq!(version.unwrap(), Version::new(1, 5, 0)); + assert!(platform.is_some()); + assert_eq!(platform.unwrap().os_family, "windows"); + + // Test with no DeviceInfo + let (version, platform) = parse_client_version_platform(None); + assert!(version.is_none()); + assert!(platform.is_none()); + + // Test with invalid version string + let info = create_device_info( + Some("invalid.version".to_string()), + Some(ClientPlatformInfo { + os_family: "linux".to_string(), + os_type: "Ubuntu".to_string(), + version: "22.04".to_string(), + ..Default::default() + }), + ); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_none()); + assert!(platform.is_some()); + + // Test with missing version field + let info = create_device_info( + None, + Some(ClientPlatformInfo { + os_family: "linux".to_string(), + os_type: "Ubuntu".to_string(), + version: "22.04".to_string(), + ..Default::default() + }), + ); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_none()); + assert!(platform.is_some()); + + // Test with missing platform field + let info = create_device_info(Some("1.5.0".to_string()), None); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_some()); + assert!(platform.is_none()); + + // Test with both fields missing + let info = create_device_info(None, None); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_none()); + assert!(platform.is_none()); + + // Test with pre-release version + let info = create_device_info( + Some("1.5.0-alpha1".to_string()), + Some(ClientPlatformInfo { + os_family: "macos".to_string(), + os_type: "macOS".to_string(), + version: "14.0".to_string(), + ..Default::default() + }), + ); + let (version, platform) = parse_client_version_platform(Some(&info)); + assert!(version.is_some()); + assert_eq!(version.unwrap(), Version::parse("1.5.0-alpha1").unwrap()); + assert!(platform.is_some()); + } + + #[test] + fn test_client_feature_is_supported_by_device() { + // Test ServiceLocations feature with supported version and OS + let info = create_device_info( + Some("1.6.0".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should be supported on Windows with version 1.6.0" + ); + + // Test with exact minimum version + let info = create_device_info( + Some("1.6.0".to_string()), + Some(ClientPlatformInfo { + os_family: "Windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should be supported at minimum version" + ); + + // Test with higher version + let info = create_device_info( + Some("2.0.0".to_string()), + Some(ClientPlatformInfo { + os_family: "WINDOWS".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should be supported with higher version" + ); + + // Test with version below minimum + let info = create_device_info( + Some("1.5.9".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported below minimum version" + ); + + // Test with wrong OS family (linux) + let info = create_device_info( + Some("1.6.0".to_string()), + Some(ClientPlatformInfo { + os_family: "linux".to_string(), + os_type: "Ubuntu".to_string(), + version: "22.04".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported on Linux" + ); + + // Test with wrong OS family (macos) + let info = create_device_info( + Some("1.6.0".to_string()), + Some(ClientPlatformInfo { + os_family: "macos".to_string(), + os_type: "macOS".to_string(), + version: "14.0".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported on macOS" + ); + + // Test with no DeviceInfo + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(None), + "ServiceLocations should not be supported without device info" + ); + + // Test with missing version + let info = create_device_info( + None, + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported without version info" + ); + + // Test with missing platform + let info = create_device_info(Some("1.6.0".to_string()), None); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported without platform info" + ); + + // Test with invalid version string + let info = create_device_info( + Some("invalid".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported with invalid version" + ); + + // Test case insensitivity of OS family matching + let info = create_device_info( + Some("1.6.0".to_string()), + Some(ClientPlatformInfo { + os_family: "WiNdOwS".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should be supported with mixed-case OS family" + ); + + // Test with pre-release version above minimum + let info = create_device_info( + Some("1.7.0-alpha1".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should be supported with pre-release version above minimum" + ); + + // Test with pre-release version below minimum + let info = create_device_info( + Some("1.5.0-alpha1".to_string()), + Some(ClientPlatformInfo { + os_family: "windows".to_string(), + os_type: "Windows".to_string(), + version: "11".to_string(), + ..Default::default() + }), + ); + assert!( + !ClientFeature::ServiceLocations.is_supported_by_device(Some(&info)), + "ServiceLocations should not be supported with pre-release version below minimum" + ); + } +} diff --git a/crates/defguard_core/src/grpc/enrollment.rs b/crates/defguard_core/src/grpc/enrollment.rs index 3194adffaa..13b0733ea5 100644 --- a/crates/defguard_core/src/grpc/enrollment.rs +++ b/crates/defguard_core/src/grpc/enrollment.rs @@ -11,6 +11,14 @@ use defguard_mail::{ Mail, templates::{self, TemplateLocation}, }; +use defguard_proto::proxy::{ + ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, + CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, Device as ProtoDevice, + DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, + EnrollmentStartResponse, ExistingDevice, InitialUserInfo, + LocationMfaMode as ProtoLocationMfaMode, MfaMethod, NewDevice, RegisterMobileAuthRequest, + ServiceLocationMode as ProtoServiceLocationMode, +}; use sqlx::{PgPool, Transaction, query_scalar}; use tokio::sync::{ broadcast::Sender, @@ -26,7 +34,7 @@ use crate::{ device::{DeviceConfig, DeviceInfo, DeviceType}, enrollment::{ENROLLMENT_TOKEN_TYPE, Token, TokenError}, polling_token::PollingToken, - wireguard::LocationMfaMode, + wireguard::{LocationMfaMode, ServiceLocationMode}, }, }, enterprise::{ @@ -35,7 +43,10 @@ use crate::{ limits::update_counts, }, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, EnrollmentEvent}, - grpc::utils::{build_device_config_response, new_polling_token, parse_client_info}, + grpc::{ + client_version::ClientFeature, + utils::{build_device_config_response, new_polling_token, parse_client_ip_agent}, + }, handlers::{ mail::{ send_email_mfa_activation_email, send_mfa_configured_email, send_new_device_added_email, @@ -45,13 +56,6 @@ use crate::{ headers::get_device_info, is_valid_phone_number, server_config, }; -use defguard_proto::proxy::{ - ActivateUserRequest, AdminInfo, CodeMfaSetupFinishRequest, CodeMfaSetupFinishResponse, - CodeMfaSetupStartRequest, CodeMfaSetupStartResponse, Device as ProtoDevice, - DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, EnrollmentStartRequest, - EnrollmentStartResponse, ExistingDevice, InitialUserInfo, - LocationMfaMode as ProtoLocationMfaMode, MfaMethod, NewDevice, RegisterMobileAuthRequest, -}; pub(super) struct EnrollmentServer { pool: PgPool, @@ -288,7 +292,7 @@ impl EnrollmentServer { })?; // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&info).map_err(Status::internal)?; let context = BidiRequestContext::new(user_id, username, ip, user_agent); self.emit_event(context, EnrollmentEvent::EnrollmentStarted) .map_err(|err| { @@ -458,6 +462,16 @@ impl EnrollmentServer { )?; } + // Unset the enrollment-pending flag (https://github.com/DefGuard/client/issues/647). + user.enrollment_pending = false; + user.save(&mut *transaction).await.map_err(|err| { + error!( + "Failed to unset enrollment_pending flag for user {}: {err}", + user.username + ); + Status::internal("unexpected error") + })?; + transaction.commit().await.map_err(|err| { error!("Failed to commit transaction: {err}"); Status::internal("unexpected error") @@ -468,7 +482,7 @@ impl EnrollmentServer { info!("User {} activated", user.username); // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&req_device_info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&req_device_info).map_err(Status::internal)?; let context = BidiRequestContext::new(user.id, user.username.clone(), ip, user_agent); self.emit_event(context, EnrollmentEvent::EnrollmentCompleted) .map_err(|err| { @@ -681,6 +695,11 @@ impl EnrollmentServer { None, true, ); + if device.name.is_empty() { + return Err(Status::invalid_argument( + "Cannot add a new device with no name. You may be trying to add a new user device as a network device. Defguard CLI supports only network devices.", + )); + } let device = device.save(&mut *transaction).await.map_err(|err| { error!( "Failed to save device {}, pubkey {} for user {}({:?}): {err}", @@ -795,6 +814,16 @@ impl EnrollmentServer { Status::internal("unexpected error") })?; + // Don't send them service locations if they don't support it + let configs = configs + .into_iter() + .filter(|config| { + config.service_location_mode == ServiceLocationMode::Disabled + || ClientFeature::ServiceLocations + .is_supported_by_device(req_device_info.as_ref()) + }) + .collect::>(); + let template_locations: Vec = configs .iter() .map(|c| TemplateLocation { @@ -843,7 +872,7 @@ impl EnrollmentServer { }; // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&req_device_info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&req_device_info).map_err(Status::internal)?; let context = BidiRequestContext::new(user.id, user.username.clone(), ip, user_agent); self.emit_event(context, EnrollmentEvent::EnrollmentDeviceAdded { device }) .map_err(|err| { @@ -859,6 +888,7 @@ impl EnrollmentServer { pub async fn get_network_info( &self, request: ExistingDevice, + device_info: Option, ) -> Result { debug!("Getting network info for device: {:?}", request.pubkey); let token = self.validate_session(request.token.as_ref()).await?; @@ -885,7 +915,7 @@ impl EnrollmentServer { } let token = new_polling_token(&self.pool, &device).await?; - build_device_config_response(&self.pool, device, Some(token)).await + build_device_config_response(&self.pool, device, Some(token), device_info).await } // TODO: Add events @@ -1065,6 +1095,12 @@ impl From for ProtoDeviceConfig { >::into(config.location_mfa_mode) .into(), ), + service_location_mode: Some( + >::into( + config.service_location_mode, + ) + .into(), + ), } } } diff --git a/crates/defguard_core/src/grpc/gateway/map.rs b/crates/defguard_core/src/grpc/gateway/map.rs index 42e665784d..60b5ab49d0 100644 --- a/crates/defguard_core/src/grpc/gateway/map.rs +++ b/crates/defguard_core/src/grpc/gateway/map.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use chrono::Utc; use defguard_common::db::Id; +use defguard_mail::Mail; use defguard_version::tracing::VersionInfo; use semver::Version; use sqlx::PgPool; @@ -10,7 +11,6 @@ use tokio::sync::mpsc::UnboundedSender; use uuid::Uuid; use super::state::GatewayState; -use defguard_mail::Mail; /// Helper struct used to handle gateway state. Gateways are grouped by network. type GatewayHostname = String; diff --git a/crates/defguard_core/src/grpc/gateway/mod.rs b/crates/defguard_core/src/grpc/gateway/mod.rs index ca3d668283..ff119fc0fc 100644 --- a/crates/defguard_core/src/grpc/gateway/mod.rs +++ b/crates/defguard_core/src/grpc/gateway/mod.rs @@ -95,11 +95,22 @@ impl WireguardNetwork { /// /// Each device is marked as allowed or not allowed in a given network, /// which enables enforcing peer disconnect in MFA-protected networks. + /// + /// If the location is a service location, only returns peers if enterprise features are enabled. pub async fn get_peers<'e, E>(&self, executor: E) -> Result, SqlxError> where E: PgExecutor<'e>, { debug!("Fetching all peers for network {}", self.id); + + if self.should_prevent_service_location_usage() { + warn!( + "Tried to use service location {} with disabled enterprise features. No clients will be allowed to connect.", + self.name + ); + return Ok(Vec::new()); + } + let rows = query!( "SELECT d.wireguard_pubkey pubkey, preshared_key, \ -- TODO possible to not use ARRAY-unnest here? @@ -228,25 +239,18 @@ impl GatewayServer { Ok(self.grpc_event_tx.send(event)?) } - /// Helper method to fetch `Device` info from DB and return appropriate errors - async fn fetch_device_from_db(&self, public_key: &str) -> Result, Status> { - let device = match Device::find_by_pubkey(&self.pool, public_key).await { - Ok(Some(device)) => device, - Ok(None) => { - error!("Device with public key {public_key} not found"); - return Err(Status::new( - Code::Internal, - format!("Device with public key {public_key} not found"), - )); - } - Err(err) => { + /// Helper method to fetch `Device` info from DB by pubkey and return appropriate errors + async fn fetch_device_from_db(&self, public_key: &str) -> Result>, Status> { + let device = Device::find_by_pubkey(&self.pool, public_key) + .await + .map_err(|err| { error!("Failed to retrieve device with public key {public_key}: {err}",); - return Err(Status::new( + Status::new( Code::Internal, format!("Failed to retrieve device with public key {public_key}: {err}",), - )); - } - }; + ) + })?; + Ok(device) } @@ -814,8 +818,17 @@ impl gateway_service_server::GatewayService for GatewayServer { // fetch device from DB // TODO: fetch only when device has changed and use client state otherwise - let device = self.fetch_device_from_db(&public_key).await?; - // copy for easier reference later + let device = match self.fetch_device_from_db(&public_key).await? { + Some(device) => device, + None => { + warn!( + "Received stats update for a device which does not exist: {public_key}, skipping." + ); + continue; + } + }; + + // copy device ID for easier reference later let device_id = device.id; // fetch user and location from DB for activity log diff --git a/crates/defguard_core/src/grpc/mod.rs b/crates/defguard_core/src/grpc/mod.rs index 9ef9ae7199..a4c4ba3dcf 100644 --- a/crates/defguard_core/src/grpc/mod.rs +++ b/crates/defguard_core/src/grpc/mod.rs @@ -1,11 +1,9 @@ use std::{ collections::hash_map::HashMap, fs::read_to_string, - time::{Duration, Instant}, -}; -use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, sync::{Arc, Mutex, RwLock}, + time::{Duration, Instant}, }; use axum::http::Uri; @@ -15,10 +13,9 @@ use defguard_common::{ db::{Id, models::Settings}, }; use defguard_mail::Mail; -use defguard_version::server::DefguardVersionLayer; use defguard_version::{ ComponentInfo, DefguardComponent, Version, client::ClientVersionInterceptor, - get_tracing_variables, + get_tracing_variables, server::DefguardVersionLayer, }; use openidconnect::{AuthorizationCode, Nonce, Scope, core::CoreAuthenticationFlow}; use reqwest::Url; @@ -40,22 +37,23 @@ use tonic::{ }; use tower::ServiceBuilder; -use self::gateway::GatewayServer; use self::{ auth::AuthServer, client_mfa::ClientMfaServer, enrollment::EnrollmentServer, - password_reset::PasswordResetServer, + gateway::GatewayServer, interceptor::JwtInterceptor, password_reset::PasswordResetServer, + worker::WorkerServer, }; -use self::{interceptor::JwtInterceptor, worker::WorkerServer}; -use crate::db::GatewayEvent; pub use crate::version::MIN_GATEWAY_VERSION; use crate::{ auth::failed_login::FailedLoginMap, db::{ - AppEvent, + AppEvent, GatewayEvent, models::enrollment::{ENROLLMENT_TOKEN_TYPE, Token}, }, enterprise::{ - db::models::{enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider}, + db::models::{ + enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, + openid_provider::OpenIdProvider, + }, directory_sync::sync_user_groups_if_configured, grpc::polling::PollingServer, handlers::openid_login::{ @@ -74,6 +72,7 @@ static VERSION_ZERO: Version = Version::new(0, 0, 0); mod auth; pub(crate) mod client_mfa; +pub mod client_version; pub mod enrollment; pub mod gateway; mod interceptor; @@ -239,7 +238,11 @@ async fn handle_proxy_message_loop( } // rpc GetNetworkInfo (ExistingDevice) returns (DeviceConfigResponse) Some(core_request::Payload::ExistingDevice(request)) => { - match context.enrollment_server.get_network_info(request).await { + match context + .enrollment_server + .get_network_info(request, received.device_info) + .await + { Ok(response_payload) => { Some(core_response::Payload::DeviceConfig(response_payload)) } @@ -350,7 +353,11 @@ async fn handle_proxy_message_loop( } // rpc LocationInfo (LocationInfoRequest) returns (LocationInfoResponse) Some(core_request::Payload::InstanceInfo(request)) => { - match context.polling_server.info(request).await { + match context + .polling_server + .info(request, received.device_info) + .await + { Ok(response_payload) => { Some(core_response::Payload::InstanceInfo(response_payload)) } @@ -381,48 +388,57 @@ async fn handle_proxy_message_loop( })) } else if let Ok(redirect_url) = Url::parse(&request.redirect_url) { if let Some(provider) = OpenIdProvider::get_current(&pool).await? { - if let Ok((_client_id, client)) = - make_oidc_client(redirect_url, &provider).await - { - let mut authorize_url_builder = client - .authorize_url( - CoreAuthenticationFlow::AuthorizationCode, - || build_state(request.state), - Nonce::new_random, - ) - .add_scope(Scope::new("email".to_string())) - .add_scope(Scope::new("profile".to_string())); - - if SELECT_ACCOUNT_SUPPORTED_PROVIDERS - .iter() - .all(|p| p.eq_ignore_ascii_case(&provider.name)) - { - authorize_url_builder = authorize_url_builder.add_prompt( - openidconnect::core::CoreAuthPrompt::SelectAccount, + match make_oidc_client(redirect_url, &provider).await { + Ok((_client_id, client)) => { + let mut authorize_url_builder = client + .authorize_url( + CoreAuthenticationFlow::AuthorizationCode, + || build_state(request.state), + Nonce::new_random, + ) + .add_scope(Scope::new("email".to_string())) + .add_scope(Scope::new("profile".to_string())); + + if SELECT_ACCOUNT_SUPPORTED_PROVIDERS + .iter() + .all(|p| p.eq_ignore_ascii_case(&provider.name)) + { + authorize_url_builder = authorize_url_builder + .add_prompt( + openidconnect::core::CoreAuthPrompt::SelectAccount, + ); + } + let (url, csrf_token, nonce) = authorize_url_builder.url(); + + Some(core_response::Payload::AuthInfo(AuthInfoResponse { + url: url.into(), + csrf_token: csrf_token.secret().to_owned(), + nonce: nonce.secret().to_owned(), + button_display_name: provider.display_name, + })) + } + Err(err) => { + error!( + "Failed to setup external OIDC provider client: {err}" ); + Some(core_response::Payload::CoreError(CoreError { + status_code: Code::Internal as i32, + message: "failed to build OIDC client".into(), + })) } - let (url, csrf_token, nonce) = authorize_url_builder.url(); - - Some(core_response::Payload::AuthInfo(AuthInfoResponse { - url: url.into(), - csrf_token: csrf_token.secret().to_owned(), - nonce: nonce.secret().to_owned(), - button_display_name: provider.display_name, - })) - } else { - Some(core_response::Payload::CoreError(CoreError { - status_code: Code::Internal as i32, - message: "failed to build OIDC client".into(), - })) } } else { error!("Failed to get current OpenID provider"); Some(core_response::Payload::CoreError(CoreError { - status_code: Code::Internal as i32, + status_code: Code::NotFound as i32, message: "failed to get current OpenID provider".into(), })) } } else { + error!( + "Invalid redirect URL in authentication info request: {}", + request.redirect_url + ); Some(core_response::Payload::CoreError(CoreError { status_code: Code::Internal as i32, message: "invalid redirect URL".into(), @@ -793,7 +809,7 @@ pub struct InstanceInfo { url: Url, proxy_url: Url, username: String, - disable_all_traffic: bool, + client_traffic_policy: ClientTrafficPolicy, enterprise_enabled: bool, openid_display_name: Option, } @@ -816,7 +832,7 @@ impl InstanceInfo { url: config.url.clone(), proxy_url: config.enrollment_url.clone(), username: username.into(), - disable_all_traffic: enterprise_settings.disable_all_traffic, + client_traffic_policy: enterprise_settings.client_traffic_policy, enterprise_enabled: is_enterprise_enabled(), openid_display_name, } @@ -831,7 +847,11 @@ impl From for defguard_proto::proxy::InstanceInfo { url: instance.url.to_string(), proxy_url: instance.proxy_url.to_string(), username: instance.username, - disable_all_traffic: instance.disable_all_traffic, + // Ensure backwards compatibility. + #[allow(deprecated)] + disable_all_traffic: instance.client_traffic_policy + == ClientTrafficPolicy::DisableAllTraffic, + client_traffic_policy: Some(instance.client_traffic_policy as i32), enterprise_enabled: instance.enterprise_enabled, openid_display_name: instance.openid_display_name, } diff --git a/crates/defguard_core/src/grpc/password_reset.rs b/crates/defguard_core/src/grpc/password_reset.rs index 9049fb449e..4e8b35e6d4 100644 --- a/crates/defguard_core/src/grpc/password_reset.rs +++ b/crates/defguard_core/src/grpc/password_reset.rs @@ -1,4 +1,8 @@ use defguard_mail::Mail; +use defguard_proto::proxy::{ + DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, + PasswordResetStartResponse, +}; use sqlx::PgPool; use tokio::sync::mpsc::{UnboundedSender, error::SendError}; use tonic::Status; @@ -10,7 +14,7 @@ use crate::{ }, enterprise::ldap::utils::ldap_change_password, events::{BidiRequestContext, BidiStreamEvent, BidiStreamEventType, PasswordResetEvent}, - grpc::utils::parse_client_info, + grpc::utils::parse_client_ip_agent, handlers::{ mail::{send_password_reset_email, send_password_reset_success_email}, user::check_password_strength, @@ -18,10 +22,6 @@ use crate::{ headers::get_device_info, server_config, }; -use defguard_proto::proxy::{ - DeviceInfo, PasswordResetInitializeRequest, PasswordResetRequest, PasswordResetStartRequest, - PasswordResetStartResponse, -}; pub(super) struct PasswordResetServer { pool: PgPool, @@ -169,7 +169,7 @@ impl PasswordResetServer { ); // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&req_device_info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&req_device_info).map_err(Status::internal)?; let context = BidiRequestContext::new(user.id, user.username, ip, user_agent); self.emit_event(context, PasswordResetEvent::PasswordResetRequested) .map_err(|err| { @@ -235,7 +235,7 @@ impl PasswordResetServer { user.username ); // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&info).map_err(Status::internal)?; let context = BidiRequestContext::new(user.id, user.username, ip, user_agent); self.emit_event(context, PasswordResetEvent::PasswordResetStarted) .map_err(|err| { @@ -308,7 +308,7 @@ impl PasswordResetServer { )?; // Prepare event context and push the event - let (ip, user_agent) = parse_client_info(&req_device_info).map_err(Status::internal)?; + let (ip, user_agent) = parse_client_ip_agent(&req_device_info).map_err(Status::internal)?; let context = BidiRequestContext::new(user.id, user.username, ip, user_agent); self.emit_event(context, PasswordResetEvent::PasswordResetCompleted) .map_err(|err| { diff --git a/crates/defguard_core/src/grpc/utils.rs b/crates/defguard_core/src/grpc/utils.rs index 757b9d9490..b82e91fa1c 100644 --- a/crates/defguard_core/src/grpc/utils.rs +++ b/crates/defguard_core/src/grpc/utils.rs @@ -4,6 +4,10 @@ use defguard_common::{ csv::AsCsv, db::{Id, models::Settings}, }; +use defguard_proto::proxy::{ + DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, DeviceInfo, + LocationMfaMode as ProtoLocationMfaMode, +}; use sqlx::PgPool; use tonic::Status; @@ -14,16 +18,13 @@ use crate::{ models::{ device::{DeviceType, WireguardNetworkDevice}, polling_token::PollingToken, - wireguard::{LocationMfaMode, WireguardNetwork}, + wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetwork}, }, }, enterprise::db::models::{ enterprise_settings::EnterpriseSettings, openid_provider::OpenIdProvider, }, -}; -use defguard_proto::proxy::{ - DeviceConfig as ProtoDeviceConfig, DeviceConfigResponse, DeviceInfo, - LocationMfaMode as ProtoLocationMfaMode, + grpc::client_version::ClientFeature, }; // Create a new token for configuration polling. @@ -72,6 +73,7 @@ pub(crate) async fn build_device_config_response( pool: &PgPool, device: Device, token: Option, + device_info: Option, ) -> Result { let settings = Settings::get_current_settings(); @@ -122,27 +124,46 @@ pub(crate) async fn build_device_config_response( ); Status::internal(format!("unexpected error: {err}")) })?; + + if network.service_location_mode != ServiceLocationMode::Disabled { + error!( + "Network device {} tried to fetch config for service location {}, which is unsupported.", + device.name, network.name + ); + return Err(Status::permission_denied( + "service location mode is not available for network devices", + )); + } + // DEPRECATED(1.5): superseeded by location_mfa_mode let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal; - let config = ProtoDeviceConfig { - config: Device::create_config(&network, &wireguard_network_device), - network_id: network.id, - network_name: network.name, - assigned_ip: wireguard_network_device.wireguard_ips.as_csv(), - endpoint: format!("{}:{}", network.endpoint, network.port), - pubkey: network.pubkey, - allowed_ips: network.allowed_ips.as_csv(), - dns: network.dns, - keepalive_interval: network.keepalive_interval, - #[allow(deprecated)] - mfa_enabled, - location_mfa_mode: Some( - >::into( - network.location_mfa_mode, - ) - .into(), - ), - }; + let config = + ProtoDeviceConfig { + config: Device::create_config(&network, &wireguard_network_device), + network_id: network.id, + network_name: network.name, + assigned_ip: wireguard_network_device.wireguard_ips.as_csv(), + endpoint: format!("{}:{}", network.endpoint, network.port), + pubkey: network.pubkey, + allowed_ips: network.allowed_ips.as_csv(), + dns: network.dns, + keepalive_interval: network.keepalive_interval, + #[allow(deprecated)] + mfa_enabled, + location_mfa_mode: Some( + >::into( + network.location_mfa_mode, + ) + .into(), + ), + service_location_mode: + Some( + >::into(network.service_location_mode) + .into(), + ), + }; configs.push(config); } } else { @@ -158,6 +179,22 @@ pub(crate) async fn build_device_config_response( ); Status::internal(format!("unexpected error: {err}")) })?; + if network.should_prevent_service_location_usage() { + warn!( + "Tried to use service location {} with disabled enterprise features.", + network.name + ); + continue; + } + if network.service_location_mode != ServiceLocationMode::Disabled + && !ClientFeature::ServiceLocations.is_supported_by_device(device_info.as_ref()) + { + info!( + "Device {} does not support service locations feature, skipping sending network {} configuration to device {}.", + device.name, network.name, device.name + ); + continue; + } // DEPRECATED(1.5): superseeded by location_mfa_mode let mfa_enabled = network.location_mfa_mode == LocationMfaMode::Internal; if let Some(wireguard_network_device) = wireguard_network_device { @@ -179,6 +216,13 @@ pub(crate) async fn build_device_config_response( ) .into(), ), + service_location_mode: + Some( + >::into(network.service_location_mode) + .into(), + ), }; configs.push(config); } @@ -187,7 +231,7 @@ pub(crate) async fn build_device_config_response( info!( "User {}({}) device {}({}) automatically fetched the newest configuration.", - user.username, user.id, device.name, device.id, + user.username, user.id, device.name, device.id ); Ok(DeviceConfigResponse { @@ -207,7 +251,7 @@ pub(crate) async fn build_device_config_response( } /// Parses `DeviceInfo` returning client IP address and user agent. -pub(crate) fn parse_client_info(info: &Option) -> Result<(IpAddr, String), String> { +pub(crate) fn parse_client_ip_agent(info: &Option) -> Result<(IpAddr, String), String> { let Some(info) = info else { error!("Missing DeviceInfo in proxy request"); return Err("missing device info".to_string()); diff --git a/crates/defguard_core/src/grpc/worker.rs b/crates/defguard_core/src/grpc/worker.rs index c0acfa12ec..069492a687 100644 --- a/crates/defguard_core/src/grpc/worker.rs +++ b/crates/defguard_core/src/grpc/worker.rs @@ -6,14 +6,14 @@ use std::{ }; use defguard_common::db::models::{AuthenticationKey, AuthenticationKeyType}; +pub use defguard_proto::worker::JobStatus; +use defguard_proto::worker::{GetJobResponse, Worker, worker_service_server}; use sqlx::{PgPool, query}; use tokio::sync::mpsc::UnboundedSender; use tonic::{Request, Response, Status}; use super::{Job, JobResponse, WorkerDetail, WorkerInfo, WorkerState}; use crate::db::{AppEvent, HWKeyUserData, User, YubiKey}; -pub use defguard_proto::worker::JobStatus; -use defguard_proto::worker::{GetJobResponse, Worker, worker_service_server}; impl WorkerInfo { /// Create new `Worker` instance. diff --git a/crates/defguard_core/src/handlers/app_info.rs b/crates/defguard_core/src/handlers/app_info.rs index e53592f9c8..344ee41925 100644 --- a/crates/defguard_core/src/handlers/app_info.rs +++ b/crates/defguard_core/src/handlers/app_info.rs @@ -1,4 +1,5 @@ use axum::{extract::State, http::StatusCode}; +use defguard_common::{VERSION, db::models::Settings}; use serde_json::json; use super::{ApiResponse, ApiResult}; @@ -13,7 +14,6 @@ use crate::{ limits::{LimitsExceeded, get_counts}, }, }; -use defguard_common::{VERSION, db::models::Settings}; #[derive(Serialize)] struct LicenseInfo { diff --git a/crates/defguard_core/src/handlers/group.rs b/crates/defguard_core/src/handlers/group.rs index a728b4ccc9..41bebed9d9 100644 --- a/crates/defguard_core/src/handlers/group.rs +++ b/crates/defguard_core/src/handlers/group.rs @@ -77,7 +77,7 @@ pub(crate) async fn bulk_assign_to_groups( "SELECT id, username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active, openid_sub, \ - from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path \ + from_ldap, ldap_pass_randomized, ldap_rdn, ldap_user_path, enrollment_pending \ FROM \"user\" WHERE id = ANY($1)", &data.users ) diff --git a/crates/defguard_core/src/handlers/mod.rs b/crates/defguard_core/src/handlers/mod.rs index cbe8263013..93263db869 100644 --- a/crates/defguard_core/src/handlers/mod.rs +++ b/crates/defguard_core/src/handlers/mod.rs @@ -12,11 +12,10 @@ use sqlx::PgPool; use utoipa::ToSchema; use webauthn_rs::prelude::RegisterPublicKeyCredential; -use crate::db::Device; use crate::{ appstate::AppState, auth::SessionInfo, - db::{User, UserInfo, WebHook}, + db::{Device, User, UserInfo, WebHook}, enterprise::{db::models::acl::AclError, license::LicenseError}, error::WebError, events::ApiRequestContext, @@ -331,6 +330,7 @@ pub struct StartEnrollmentRequest { #[serde(default)] pub send_enrollment_notification: bool, pub email: Option, + pub token_expiration_time: Option, } #[derive(Deserialize, Serialize, ToSchema)] diff --git a/crates/defguard_core/src/handlers/user.rs b/crates/defguard_core/src/handlers/user.rs index af6f4534aa..128522ddd4 100644 --- a/crates/defguard_core/src/handlers/user.rs +++ b/crates/defguard_core/src/handlers/user.rs @@ -5,6 +5,7 @@ use axum::{ http::StatusCode, }; use defguard_mail::{Mail, templates}; +use humantime::parse_duration; use serde_json::json; use super::{ @@ -418,7 +419,7 @@ pub async fn start_enrollment( "Search for the user {} in database to get started with enrollment process.", username ); - let Some(user) = User::find_by_username(&appstate.pool, &username).await? else { + let Some(mut user) = User::find_by_username(&appstate.pool, &username).await? else { error!("User {username} couldn't be found, enrollment aborted"); return Err(WebError::ObjectNotFound(format!( "user {username} not found" @@ -428,20 +429,31 @@ pub async fn start_enrollment( debug!("Create a new database transaction to save a new enrollment token into the database."); let mut transaction = appstate.pool.begin().await?; + // try to parse token expiration time if provided let config = server_config(); + let token_expiration_time_seconds = match data.token_expiration_time { + Some(time) => parse_duration(&time) + .map_err(|err| { + error!("Failed to parse token expiration time {time}: {err}"); + WebError::BadRequest("Failed to parse token expiration time".to_owned()) + })? + .as_secs(), + None => config.enrollment_token_timeout.as_secs(), + }; + let enrollment_token = user .start_enrollment( &mut transaction, &session.user, data.email, - config.enrollment_token_timeout.as_secs(), + token_expiration_time_seconds, config.enrollment_url.clone(), data.send_enrollment_notification, appstate.mail_tx.clone(), ) .await?; - debug!("Try to commit transaction to save the enrollment token into the databse."); + debug!("Try to commit transaction to save the enrollment token into the database."); transaction.commit().await?; debug!("Transaction committed."); diff --git a/crates/defguard_core/src/handlers/wireguard.rs b/crates/defguard_core/src/handlers/wireguard.rs index 466cc7cb62..9410134bdd 100644 --- a/crates/defguard_core/src/handlers/wireguard.rs +++ b/crates/defguard_core/src/handlers/wireguard.rs @@ -31,8 +31,9 @@ use crate::{ WireguardNetworkDevice, }, wireguard::{ - DateTimeAggregation, LocationMfaMode, MappedDevice, WireguardDeviceStatsRow, - WireguardNetworkInfo, WireguardNetworkStats, WireguardUserStatsRow, networks_stats, + DateTimeAggregation, LocationMfaMode, MappedDevice, ServiceLocationMode, + WireguardDeviceStatsRow, WireguardNetworkInfo, WireguardNetworkStats, + WireguardUserStatsRow, networks_stats, }, }, }, @@ -85,6 +86,7 @@ pub struct WireguardNetworkData { pub acl_enabled: bool, pub acl_default_allow: bool, pub location_mfa_mode: LocationMfaMode, + pub service_location_mode: ServiceLocationMode, } impl WireguardNetworkData { @@ -94,6 +96,29 @@ impl WireguardNetworkData { .map_or(Vec::new(), |ips| parse_network_address_list(ips)) } + pub(crate) fn parse_addresses(&self) -> Result, WebError> { + // first parse the addresses + let subnets = parse_address_list(self.address.as_ref()); + + // check if address list is not empty + if subnets.is_empty() { + return Err(WebError::BadRequest( + "Must provide at least one valid network address".to_owned(), + )); + } + + // check if any subnet has an invalid /0 netmask + for subnet in &subnets { + if subnet.prefix() == 0 { + return Err(WebError::BadRequest(format!( + "{subnet} is not a valid address" + ))); + } + } + + Ok(subnets) + } + pub(crate) async fn validate_location_mfa_mode<'e, E: sqlx::PgExecutor<'e>>( &self, executor: E, @@ -196,6 +221,7 @@ pub(crate) async fn create_network( data.acl_enabled, data.acl_default_allow, data.location_mfa_mode, + data.service_location_mode, ); let mut transaction = appstate.pool.begin().await?; @@ -278,6 +304,8 @@ pub(crate) async fn modify_network( let mut network = find_network(network_id, &appstate.pool).await?; // store network before mods let before = network.clone(); + network.address = data.parse_addresses()?; + network.allowed_ips = data.parse_allowed_ips(); network.name = data.name; @@ -287,11 +315,20 @@ pub(crate) async fn modify_network( network.endpoint = data.endpoint; network.port = data.port; network.dns = data.dns; - network.address = parse_address_list(&data.address); network.keepalive_interval = data.keepalive_interval; network.peer_disconnect_threshold = data.peer_disconnect_threshold; network.acl_enabled = data.acl_enabled; network.acl_default_allow = data.acl_default_allow; + network.service_location_mode = match data.location_mfa_mode { + LocationMfaMode::Disabled => data.service_location_mode, + _ => { + warn!( + "Disabling service location mode for location {} because location MFA is enabled", + network.name + ); + ServiceLocationMode::Disabled + } + }; network.location_mfa_mode = data.location_mfa_mode; network.save(&mut *transaction).await?; @@ -716,7 +753,8 @@ pub struct AddDeviceResult { "pubkey": "pubkey", "dns": "8.8.8.8", "keepalive_interval": 5, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" } ], "device": { diff --git a/crates/defguard_core/src/lib.rs b/crates/defguard_core/src/lib.rs index 5e200f2725..0c4f17c3c4 100644 --- a/crates/defguard_core/src/lib.rs +++ b/crates/defguard_core/src/lib.rs @@ -6,7 +6,6 @@ use std::{ sync::{Arc, LazyLock, Mutex, RwLock}, }; -use crate::version::IncompatibleComponents; use anyhow::anyhow; use axum::{ Extension, Json, Router, @@ -89,31 +88,19 @@ use utoipa::{ }; use utoipa_swagger_ui::SwaggerUi; -use self::handlers::wireguard::{ - add_device, add_user_devices, create_network, create_network_token, delete_device, - delete_network, devices_stats, download_config, gateway_status, get_device, import_network, - list_devices, list_networks, list_user_devices, modify_device, modify_network, network_details, - network_stats, remove_gateway, -}; -use self::handlers::worker::{ - create_job, create_worker_token, job_status, list_workers, remove_worker, -}; -use self::handlers::{ - openid_clients::{ - add_openid_client, change_openid_client, change_openid_client_state, delete_openid_client, - get_openid_client, list_openid_clients, - }, - openid_flow::{ - authorization, discovery_keys, openid_configuration, secure_authorization, token, userinfo, - }, -}; use self::{ appstate::AppState, + auth::failed_login::FailedLoginMap, db::{ AppEvent, Device, GatewayEvent, User, WireguardNetwork, - models::wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + models::{ + oauth2client::OAuth2Client, + wireguard::{DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL}, + }, }, + grpc::{WorkerState, gateway::map::GatewayMap}, handlers::{ + app_info::get_app_info, auth::{ authenticate, email_mfa_code, email_mfa_disable, email_mfa_enable, email_mfa_init, logout, mfa_disable, mfa_enable, recovery_code, request_email_mfa_code, totp_code, @@ -126,6 +113,14 @@ use self::{ remove_group_member, }, mail::{send_support_data, test_mail}, + openid_clients::{ + add_openid_client, change_openid_client, change_openid_client_state, + delete_openid_client, get_openid_client, list_openid_clients, + }, + openid_flow::{ + authorization, discovery_keys, openid_configuration, secure_authorization, token, + userinfo, + }, settings::{ get_settings, get_settings_essentials, patch_settings, set_default_branding, test_ldap_settings, update_settings, @@ -142,14 +137,16 @@ use self::{ webhooks::{ add_webhook, change_enabled, change_webhook, delete_webhook, get_webhook, list_webhooks, }, + wireguard::{ + add_device, add_user_devices, create_network, create_network_token, delete_device, + delete_network, devices_stats, download_config, gateway_status, get_device, + import_network, list_devices, list_networks, list_user_devices, modify_device, + modify_network, network_details, network_stats, remove_gateway, + }, + worker::{create_job, create_worker_token, job_status, list_workers, remove_worker}, }, }; -use self::{ - auth::failed_login::FailedLoginMap, - db::models::oauth2client::OAuth2Client, - grpc::{WorkerState, gateway::map::GatewayMap}, - handlers::app_info::get_app_info, -}; +use crate::{db::models::wireguard::ServiceLocationMode, version::IncompatibleComponents}; pub mod appstate; pub mod auth; @@ -777,6 +774,7 @@ pub async fn init_dev_env(config: &DefGuardConfig) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ); network.pubkey = "zGMeVGm9HV9I4wSKF9AXmYnnAIhDySyqLMuKpcfIaQo=".to_string(); network.prvkey = "MAk3d5KuB167G88HM7nGYR6ksnPMAOguAg2s5EcPp1M=".to_string(); @@ -874,6 +872,7 @@ pub async fn init_vpn_location( false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&mut *transaction) .await?; @@ -913,6 +912,7 @@ pub async fn init_vpn_location( false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(pool) .await? diff --git a/crates/defguard_core/src/support.rs b/crates/defguard_core/src/support.rs index 753daa5c5e..e37bf4e770 100644 --- a/crates/defguard_core/src/support.rs +++ b/crates/defguard_core/src/support.rs @@ -1,5 +1,9 @@ use std::{collections::HashMap, fmt::Display}; +use defguard_common::{ + VERSION, + db::{Id, models::Settings}, +}; use serde::Serialize; use serde_json::{Value, json, value::to_value}; use sqlx::PgPool; @@ -8,10 +12,6 @@ use crate::{ db::{User, WireguardNetwork, models::device::WireguardNetworkDevice}, server_config, }; -use defguard_common::{ - VERSION, - db::{Id, models::Settings}, -}; /// Unwraps the result returning a JSON representation of value or error fn unwrap_json(result: Result) -> Value { diff --git a/crates/defguard_core/src/utility_thread.rs b/crates/defguard_core/src/utility_thread.rs index 6533ff71c5..5a0de67b32 100644 --- a/crates/defguard_core/src/utility_thread.rs +++ b/crates/defguard_core/src/utility_thread.rs @@ -9,7 +9,7 @@ use tokio::{ use tracing::Instrument; use crate::{ - db::{GatewayEvent, WireguardNetwork}, + db::{GatewayEvent, WireguardNetwork, models::wireguard::ServiceLocationMode}, enterprise::{ db::models::acl::{AclRule, RuleState}, directory_sync::{do_directory_sync, get_directory_sync_interval}, @@ -172,25 +172,51 @@ async fn enterprise_status_check( if new_enterprise_enabled { // handle switch from disabled -> enabled debug!("Re-enabling gateway firewall configuration for ACL-enabled locations"); - let mut conn = pool.acquire().await?; + let mut transaction = pool.begin().await?; for location in locations { debug!("Re-enabling gateway firewall configuration for location {location:?}"); let firewall_config = location - .try_get_firewall_config(&mut conn) + .try_get_firewall_config(&mut transaction) .await? .expect("ACL-enabled location must have firewall config"); - wireguard_tx.send(GatewayEvent::FirewallConfigChanged( - location.id, - firewall_config, - ))?; + // Handle service location update or just update the firewall + if location.service_location_mode != ServiceLocationMode::Disabled { + let new_peers = location.get_peers(&mut *transaction).await?; + wireguard_tx.send(GatewayEvent::NetworkModified( + location.id, + location, + new_peers, + Some(firewall_config), + ))?; + } else { + wireguard_tx.send(GatewayEvent::FirewallConfigChanged( + location.id, + firewall_config, + ))?; + } } + transaction.commit().await?; } else { // handle switch from enabled -> disabled debug!("Disabling gateway firewall configuration for ACL-enabled locations"); for location in locations { - debug!("Disabling gateway firewall configuration for location {location:?}"); - wireguard_tx.send(GatewayEvent::FirewallDisabled(location.id))?; + if location.service_location_mode != ServiceLocationMode::Disabled { + debug!( + "Disabling gateway firewall configuration and service location client connections \ + for location {location:?}" + ); + wireguard_tx.send(GatewayEvent::NetworkModified( + location.id, + location, + // Send empty peer list, we are disabling the service location + Vec::new(), + None, + ))?; + } else { + debug!("Disabling gateway firewall configuration for location {location:?}"); + wireguard_tx.send(GatewayEvent::FirewallDisabled(location.id))?; + } } } } diff --git a/crates/defguard_core/src/version.rs b/crates/defguard_core/src/version.rs index 5285e2e2eb..849c232337 100644 --- a/crates/defguard_core/src/version.rs +++ b/crates/defguard_core/src/version.rs @@ -5,12 +5,11 @@ use std::{ }; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use defguard_version::{ComponentInfo, Version, is_version_lower}; use serde::Serialize; use tonic::{Status, service::Interceptor}; -use defguard_version::{ComponentInfo, Version, is_version_lower}; - -const MIN_PROXY_VERSION: Version = Version::new(1, 5, 0); +const MIN_PROXY_VERSION: Version = Version::new(1, 6, 0); pub const MIN_GATEWAY_VERSION: Version = Version::new(1, 5, 0); static OUTDATED_COMPONENT_LIFETIME: TimeDelta = TimeDelta::hours(1); diff --git a/crates/defguard_core/src/wg_config.rs b/crates/defguard_core/src/wg_config.rs index 2a42ea68dd..38d40c6f0c 100644 --- a/crates/defguard_core/src/wg_config.rs +++ b/crates/defguard_core/src/wg_config.rs @@ -11,7 +11,7 @@ use crate::{ Device, WireguardNetwork, models::wireguard::{ DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode, - WireguardNetworkError, + ServiceLocationMode, WireguardNetworkError, }, }, }; @@ -112,6 +112,7 @@ pub(crate) fn parse_wireguard_config( false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ); network.pubkey = pubkey; network.prvkey = prvkey.to_string(); @@ -170,9 +171,10 @@ pub(crate) fn parse_wireguard_config( #[cfg(test)] mod test { - use super::*; use defguard_common::db::NoId; + use super::*; + #[test] fn test_parse_config() { let config = " diff --git a/crates/defguard_core/src/wireguard_peer_disconnect.rs b/crates/defguard_core/src/wireguard_peer_disconnect.rs index 537db19a30..12a5d02acb 100644 --- a/crates/defguard_core/src/wireguard_peer_disconnect.rs +++ b/crates/defguard_core/src/wireguard_peer_disconnect.rs @@ -27,7 +27,7 @@ use crate::{ Device, GatewayEvent, WireguardNetwork, models::{ device::{DeviceInfo, DeviceNetworkInfo, DeviceType, WireguardNetworkDevice}, - wireguard::{LocationMfaMode, WireguardNetworkError}, + wireguard::{LocationMfaMode, ServiceLocationMode, WireguardNetworkError}, }, }, events::{InternalEvent, InternalEventContext}, @@ -97,7 +97,8 @@ pub async fn run_periodic_peer_disconnect( "SELECT \ id, name, address, port, pubkey, prvkey, endpoint, dns, allowed_ips, \ connected_at, keepalive_interval, peer_disconnect_threshold, \ - acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\" \ + acl_enabled, acl_default_allow, location_mfa_mode \"location_mfa_mode: LocationMfaMode\", \ + service_location_mode \"service_location_mode: ServiceLocationMode\" \ FROM wireguard_network WHERE location_mfa_mode != 'disabled'::location_mfa_mode", ) .fetch_all(&pool) diff --git a/crates/defguard_core/tests/integration/api/acl.rs b/crates/defguard_core/tests/integration/api/acl.rs index 237b573fdd..1272d5d8d9 100644 --- a/crates/defguard_core/tests/integration/api/acl.rs +++ b/crates/defguard_core/tests/integration/api/acl.rs @@ -5,7 +5,10 @@ use defguard_common::{ use defguard_core::{ db::{ Device, Group, User, WireguardNetwork, - models::{device::DeviceType, wireguard::LocationMfaMode}, + models::{ + device::DeviceType, + wireguard::{LocationMfaMode, ServiceLocationMode}, + }, }, enterprise::{ db::models::acl::{AclAlias, AclRule, AliasKind, AliasState, RuleState}, @@ -137,8 +140,8 @@ fn edit_alias_data_into_api_response( async fn test_rule_crud(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; let rule = make_rule(); @@ -189,8 +192,8 @@ async fn test_rule_crud(_: PgPoolOptions, options: PgConnectOptions) { async fn test_rule_enterprise(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; exceed_enterprise_limits(&client).await; @@ -230,8 +233,8 @@ async fn test_rule_enterprise(_: PgPoolOptions, options: PgConnectOptions) { async fn test_alias_crud(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; let alias = make_alias(); @@ -284,8 +287,8 @@ async fn test_alias_crud(_: PgPoolOptions, options: PgConnectOptions) { async fn test_alias_enterprise(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; exceed_enterprise_limits(&client).await; @@ -325,8 +328,8 @@ async fn test_alias_enterprise(_: PgPoolOptions, options: PgConnectOptions) { async fn test_empty_strings(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; // rule let mut rule = make_rule(); @@ -409,8 +412,8 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; // create related objects // networks @@ -427,6 +430,7 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -541,8 +545,8 @@ async fn test_related_objects(_: PgPoolOptions, options: PgConnectOptions) { async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, state) = make_test_client(pool).await; + authenticate_admin(&mut client).await; let rule = make_rule(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; @@ -644,8 +648,8 @@ async fn test_invalid_related_objects(_: PgPoolOptions, options: PgConnectOption async fn test_invalid_data(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _) = make_test_client(pool).await; + authenticate_admin(&mut client).await; // invalid port let mut rule = make_rule(); @@ -677,8 +681,8 @@ async fn test_rule_create_modify_state(_: PgPoolOptions, options: PgConnectOptio let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let rule = make_rule(); @@ -732,8 +736,8 @@ async fn test_rule_delete_state_new(_: PgPoolOptions, options: PgConnectOptions) let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; // test NEW rule deletion let rule = make_rule(); @@ -751,8 +755,8 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; // create a location WireguardNetwork::new( @@ -767,6 +771,7 @@ async fn test_rule_delete_state_applied(_: PgPoolOptions, options: PgConnectOpti false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -812,8 +817,8 @@ async fn test_rule_duplication(_: PgPoolOptions, options: PgConnectOptions) { // each modification / deletion of parent rule should remove the child and create a new one let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let rule = make_rule(); let response = client.post("/api/v1/acl/rule").json(&rule).send().await; @@ -842,8 +847,8 @@ async fn test_rule_application(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let rule = make_rule(); @@ -934,8 +939,8 @@ async fn test_multiple_rules_application(_: PgPoolOptions, options: PgConnectOpt let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let rule_1 = make_rule(); let rule_2 = make_rule(); @@ -972,8 +977,8 @@ async fn test_alias_create_modify_state(_: PgPoolOptions, options: PgConnectOpti let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let alias = make_alias(); @@ -1012,8 +1017,8 @@ async fn test_alias_delete(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; // create alias let alias = make_alias(); @@ -1078,8 +1083,8 @@ async fn test_alias_duplication(_: PgPoolOptions, options: PgConnectOptions) { // each modification of parent alias should remove the child and create a new one let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let alias = make_alias(); let response = client.post("/api/v1/acl/alias").json(&alias).send().await; @@ -1104,8 +1109,8 @@ async fn test_alias_application(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; // create new alias let alias = make_alias(); @@ -1165,8 +1170,8 @@ async fn test_multiple_aliases_application(_: PgPoolOptions, options: PgConnectO let pool = setup_pool(options).await; let config = init_config(None); - let client = make_client_v2(pool.clone(), config).await; - authenticate_admin(&client).await; + let mut client = make_client_v2(pool.clone(), config).await; + authenticate_admin(&mut client).await; let alias_1 = make_alias(); let alias_2 = make_alias(); diff --git a/crates/defguard_core/tests/integration/api/auth.rs b/crates/defguard_core/tests/integration/api/auth.rs index ad43507125..60d3c29e2b 100644 --- a/crates/defguard_core/tests/integration/api/auth.rs +++ b/crates/defguard_core/tests/integration/api/auth.rs @@ -6,6 +6,7 @@ use defguard_common::db::models::{MFAMethod, Settings, settings::update_current_ use defguard_core::{ auth::{TOTP_CODE_DIGITS, TOTP_CODE_VALIDITY_PERIOD}, db::{MFAInfo, User, UserDetails}, + events::ApiEventType, handlers::{Auth, AuthCode, AuthResponse, AuthTotp}, }; use reqwest::{StatusCode, header::USER_AGENT}; @@ -19,12 +20,11 @@ use totp_lite::{Sha1, totp_custom}; use webauthn_authenticator_rs::{WebauthnAuthenticator, prelude::Url, softpasskey::SoftPasskey}; use webauthn_rs::prelude::{CreationChallengeResponse, RequestChallengeResponse}; -use crate::api::common::client::TestResponse; - use super::common::{ X_FORWARDED_FOR, fetch_user_details, make_client, make_client_with_db, make_test_client, setup_pool, }; +use crate::api::common::client::TestResponse; static SESSION_COOKIE_NAME: &str = "defguard_session"; @@ -59,6 +59,8 @@ async fn test_logout(_: PgPoolOptions, options: PgConnectOptions) { let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::OK); + client.verify_api_events_with_user(&[(ApiEventType::UserLogin, 2, "hpotter")]); + // store auth cookie for later use let auth_cookie = response .cookies() @@ -74,6 +76,8 @@ async fn test_logout(_: PgPoolOptions, options: PgConnectOptions) { let response = client.get("/api/v1/me").send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + client.verify_api_events_with_user(&[(ApiEventType::UserLogout, 2, "hpotter")]); + // try reusing auth cookie client.set_cookie(&auth_cookie); let response = client.get("/api/v1/me").send().await; @@ -84,7 +88,7 @@ async fn test_logout(_: PgPoolOptions, options: PgConnectOptions) { async fn test_login_bruteforce(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; let invalid_auth = Auth::new("hpotter", "invalid"); @@ -93,8 +97,12 @@ async fn test_login_bruteforce(_: PgPoolOptions, options: PgConnectOptions) { let response = client.post("/api/v1/auth").json(&invalid_auth).send().await; if i == 5 { assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); + client.assert_event_queue_is_empty(); } else { assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + client.verify_api_events(&[ApiEventType::UserLoginFailed { + message: "Authentication for hpotter failed: invalid password".into(), + }]); } } } diff --git a/crates/defguard_core/tests/integration/api/common/client.rs b/crates/defguard_core/tests/integration/api/common/client.rs index 9292b31a3e..2203c0c899 100644 --- a/crates/defguard_core/tests/integration/api/common/client.rs +++ b/crates/defguard_core/tests/integration/api/common/client.rs @@ -2,22 +2,29 @@ use std::{net::SocketAddr, sync::Arc}; use axum::{Router, serve}; use bytes::Bytes; -use defguard_core::events::ApiEvent; +use defguard_common::db::Id; +use defguard_core::{ + events::{ApiEvent, ApiEventType}, + handlers::Auth, +}; use reqwest::{ Body, Client, StatusCode, Url, cookie::{Cookie, Jar}, header::{HeaderMap, HeaderName, HeaderValue, USER_AGENT}, redirect::Policy, }; -use tokio::{net::TcpListener, sync::mpsc::UnboundedReceiver, task::JoinHandle}; +use tokio::{ + net::TcpListener, + sync::mpsc::{UnboundedReceiver, error::TryRecvError}, + task::JoinHandle, +}; pub struct TestClient { client: Client, jar: Arc, port: u16, - // Has to live during whole test - #[allow(dead_code)] api_event_rx: UnboundedReceiver, + // Has to live during whole test api_task_handle: JoinHandle<()>, } @@ -65,6 +72,15 @@ impl TestClient { .add_cookie_str(&format!("{}={}", cookie.name(), cookie.value()), &url); } + // Helper to perform API login + pub async fn login_user(&mut self, username: &str, password: &str) { + let auth = Auth::new(username, password); + let response = self.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + self.verify_api_events(&[ApiEventType::UserLogin]); + } + /// returns the base URL (http://ip:port) for this TestClient /// /// this is useful when trying to check if Location headers in responses @@ -122,6 +138,101 @@ impl TestClient { builder: self.client.delete(full_url), } } + + /// Assert that expected API events have been emitted + /// + /// `expected_events` should include all events that are currently in the queue + /// for the assertions to pass. + /// If there are too many or not enough events in the queue this should panic. + pub fn verify_api_events(&mut self, expected_events: &[ApiEventType]) { + // take all the events from the queue + let events = self.drain_all_events(); + + // verify number of events + assert_eq!( + events.len(), + expected_events.len(), + "Event number different than expected" + ); + + // compare events in order + for (index, (expected_event, (event, _user_id, _username))) in + expected_events.iter().zip(events.iter()).enumerate() + { + assert_eq!( + expected_event, event, + "Mismatch at index {index}: expected {expected_event:?}, got {event:?}", + ); + } + } + + /// A variant of `verify_api_events` which also compares user context + /// + /// Other parts of event context that would be hard and not that useful to test (timestamp, device) are omitted. + pub fn verify_api_events_with_user(&mut self, expected_events: &[(ApiEventType, Id, &str)]) { + // take all the events from the queue + let events = self.drain_all_events(); + + // verify number of events + assert_eq!( + events.len(), + expected_events.len(), + "Event number different than expected" + ); + + // compare events in order + for ( + index, + ((expected_event, expected_user_id, expected_username), (event, user_id, username)), + ) in expected_events.iter().zip(events.iter()).enumerate() + { + assert_eq!( + expected_event, event, + "Event type mismatch at index {index}: expected {expected_event:?}, got {event:?}", + ); + assert_eq!( + expected_user_id, user_id, + "User ID mismatch at index {index}: expected {expected_user_id:?}, got {user_id:?}", + ); + assert_eq!( + expected_username, username, + "Username mismatch at index {index}: expected {expected_username:?}, got {username:?}", + ); + } + } + + /// Receive all messages currently present in API event queue + /// + /// Can also be used to clear the queue. + pub fn drain_all_events(&mut self) -> Vec<(ApiEventType, Id, String)> { + let mut all_events = Vec::new(); + + loop { + match self.api_event_rx.try_recv() { + Ok(msg) => all_events.push((*msg.event, msg.context.user_id, msg.context.username)), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => { + // No more messages available right now + break; + } + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { + // Channel is closed + break; + } + } + } + all_events + } + + /// Assert there are no events queued + pub fn assert_event_queue_is_empty(&mut self) { + match self.api_event_rx.try_recv() { + Err(TryRecvError::Empty) => { + // Queue is empty, test passes + } + Ok(msg) => panic!("Expected empty queue, but got event: {msg:?}"), + Err(TryRecvError::Disconnected) => panic!("Channel is disconnected"), + } + } } impl Drop for TestClient { diff --git a/crates/defguard_core/tests/integration/api/common/mod.rs b/crates/defguard_core/tests/integration/api/common/mod.rs index 669ecc9649..1c4e222445 100644 --- a/crates/defguard_core/tests/integration/api/common/mod.rs +++ b/crates/defguard_core/tests/integration/api/common/mod.rs @@ -14,7 +14,7 @@ use defguard_common::{ use defguard_core::{ auth::failed_login::FailedLoginMap, build_webapp, - db::{AppEvent, GatewayEvent, User, UserDetails}, + db::{AppEvent, Device, GatewayEvent, User, UserDetails, WireguardNetwork}, enterprise::license::{License, set_cached_license}, events::ApiEvent, grpc::{WorkerState, gateway::map::GatewayMap}, @@ -181,7 +181,8 @@ pub(crate) async fn exceed_enterprise_limits(client: &TestClient) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -201,7 +202,8 @@ pub(crate) async fn exceed_enterprise_limits(client: &TestClient) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -221,7 +223,8 @@ pub(crate) fn make_network() -> Value { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }) } @@ -241,8 +244,27 @@ pub(crate) async fn make_client_with_db(pool: PgPool) -> (TestClient, PgPool) { (client, client_state.pool) } -pub(crate) async fn authenticate_admin(client: &TestClient) { - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); +pub(crate) async fn authenticate_admin(client: &mut TestClient) { + client.login_user("admin", "pass123").await; +} + +// Helper to fetch current user state from DB by username +pub(crate) async fn get_db_user(pool: &PgPool, username: &str) -> User { + User::find_by_username(pool, username) + .await + .unwrap() + .unwrap() +} + +// Helper to fetch current location state from DB by ID +pub(crate) async fn get_db_location(pool: &PgPool, location_id: Id) -> WireguardNetwork { + WireguardNetwork::find_by_id(pool, location_id) + .await + .unwrap() + .unwrap() +} + +// Helper to fetch current user device state from DB by device ID +pub(crate) async fn get_db_device(pool: &PgPool, device_id: Id) -> Device { + Device::find_by_id(pool, device_id).await.unwrap().unwrap() } diff --git a/crates/defguard_core/tests/integration/api/enrollment.rs b/crates/defguard_core/tests/integration/api/enrollment.rs index 7068650a3d..5ca0c8eea5 100644 --- a/crates/defguard_core/tests/integration/api/enrollment.rs +++ b/crates/defguard_core/tests/integration/api/enrollment.rs @@ -1,5 +1,6 @@ +use chrono::Duration; use defguard_core::{ - db::models::enrollment::Token, + db::{User, models::enrollment::Token}, handlers::{AddUserData, Auth}, }; use reqwest::StatusCode; @@ -120,3 +121,249 @@ async fn test_enroll_disabled_user(_: PgPoolOptions, options: PgConnectOptions) .await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } + +#[sqlx::test] +async fn test_enrollment_pending_unset_for_regular_user( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create user with password + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: Some("Password1234543$!".into()), + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + let user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + + // verify enrollment_pending flag is not set + assert!(!user.enrollment_pending); + + // verify user is considered enrolled + assert!(user.is_enrolled()); +} + +#[sqlx::test] +async fn test_request_enrollment(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create user without password + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: None, + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + let user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + + // verify enrollment token was not created + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 0); + + // verify enrollment variables + assert!(!user.enrollment_pending); + assert!(!user.is_enrolled()); + + // request enrollment + let response = client + .post(format!("/api/v1/user/{}/start_enrollment", user.username)) + .json(&json!({"email": user.email, "send_enrollment_notification": false})) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // re-fetch the user + let user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + + // verify enrollment variables + assert!(user.enrollment_pending); + assert!(!user.is_enrolled()); + + // verify enrollment token was created correctly + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert!(token.used_at.is_none()); +} + +#[sqlx::test] +async fn test_enrollment_token_expiration_time(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create user without password + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: None, + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + let user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + + // verify enrollment token was not created + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 0); + + // request enrollment + let response = client + .post(format!("/api/v1/user/{}/start_enrollment", user.username)) + .json(&json!({"email": user.email, "send_enrollment_notification": false})) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was created with default expiration time (24h) + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert_eq!(token.expires_at, token.created_at + Duration::hours(24)); + + // request enrollment with different expiration time + let response = client + .post(format!("/api/v1/user/{}/start_enrollment", user.username)) + .json(&json!({"email": user.email, "send_enrollment_notification": false, "token_expiration_time": "3d"})) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was created with default expiration time (24h) + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert_eq!(token.expires_at, token.created_at + Duration::hours(72)); + + // request enrollment with different expiration time + let response = client + .post(format!("/api/v1/user/{}/start_enrollment", user.username)) + .json(&json!({"email": user.email, "send_enrollment_notification": false, "token_expiration_time": "1w"})) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was created with default expiration time (24h) + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert_eq!(token.expires_at, token.created_at + Duration::days(7)); + + // request enrollment with different expiration time + let response = client + .post(format!("/api/v1/user/{}/start_enrollment", user.username)) + .json(&json!({"email": user.email, "send_enrollment_notification": false, "token_expiration_time": "2h"})) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was created with default expiration time (24h) + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert_eq!(token.expires_at, token.created_at + Duration::hours(2)); +} + +#[sqlx::test] +async fn test_enrollment_pending_unset_for_desktop_client( + _: PgPoolOptions, + options: PgConnectOptions, +) { + let pool = setup_pool(options).await; + + let (client, pool) = make_client_with_db(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create user with password + let new_user = AddUserData { + username: "adumbledore".into(), + last_name: "Dumbledore".into(), + first_name: "Albus".into(), + email: "a.dumbledore@hogwart.edu.uk".into(), + phone: Some("1234".into()), + password: Some("Password1234543$!".into()), + }; + let response = client.post("/api/v1/user").json(&new_user).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was not created + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 0); + + let user = User::find_by_username(&pool, &new_user.username) + .await + .unwrap() + .unwrap(); + + // verify enrollment variables + assert!(!user.enrollment_pending); + assert!(user.is_enrolled()); + + // request device configuration + let response = client + .post("/api/v1/user/adumbledore/start_desktop") + .json(&json!({ + "username": user.username, + "email": user.email, + "send_enrollment_notification": false + })) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // verify enrollment token was created correctly + let tokens = Token::fetch_all(&pool).await.unwrap(); + assert_eq!(tokens.len(), 1); + let token = tokens.first().unwrap(); + assert!(token.used_at.is_none()); + + // verify enrollment variables + assert!(!user.enrollment_pending); + assert!(user.is_enrolled()); +} diff --git a/crates/defguard_core/tests/integration/api/enterprise_settings.rs b/crates/defguard_core/tests/integration/api/enterprise_settings.rs index f24582a659..c065dde6cc 100644 --- a/crates/defguard_core/tests/integration/api/enterprise_settings.rs +++ b/crates/defguard_core/tests/integration/api/enterprise_settings.rs @@ -1,6 +1,6 @@ use defguard_core::{ enterprise::{ - db::models::enterprise_settings::EnterpriseSettings, + db::models::enterprise_settings::{ClientTrafficPolicy, EnterpriseSettings}, license::{get_cached_license, set_cached_license}, }, handlers::Auth, @@ -33,7 +33,7 @@ async fn test_only_enterprise_can_modify_enterpise_settings( // try to patch enterprise settings let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; @@ -81,7 +81,7 @@ async fn test_admin_devices_management_is_enforced(_: PgPoolOptions, options: Pg // setup admin devices management let settings = EnterpriseSettings { admin_device_management: true, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; let response = client @@ -177,7 +177,7 @@ async fn test_regular_user_device_management(_: PgPoolOptions, options: PgConnec // setup admin devices management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: false, }; let response = client @@ -265,7 +265,7 @@ async fn dg25_12_test_enforce_client_activation_only(_: PgPoolOptions, options: // disable manual device management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: true, }; let response = client @@ -346,7 +346,7 @@ async fn dg25_13_test_disable_device_config(_: PgPoolOptions, options: PgConnect // disable manual device management let settings = EnterpriseSettings { admin_device_management: false, - disable_all_traffic: false, + client_traffic_policy: ClientTrafficPolicy::None, only_client_activation: true, }; let response = client diff --git a/crates/defguard_core/tests/integration/api/openid_login.rs b/crates/defguard_core/tests/integration/api/openid_login.rs index c08353e428..923633fe1a 100644 --- a/crates/defguard_core/tests/integration/api/openid_login.rs +++ b/crates/defguard_core/tests/integration/api/openid_login.rs @@ -53,6 +53,7 @@ async fn test_openid_providers(_: PgPoolOptions, options: PgConnectOptions) { directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client @@ -153,6 +154,7 @@ async fn test_openid_login(_: PgPoolOptions, options: PgConnectOptions) { directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client .post("/api/v1/openid/provider") diff --git a/crates/defguard_core/tests/integration/api/snat.rs b/crates/defguard_core/tests/integration/api/snat.rs index 01655d9b56..3e0ad6428a 100644 --- a/crates/defguard_core/tests/integration/api/snat.rs +++ b/crates/defguard_core/tests/integration/api/snat.rs @@ -20,10 +20,10 @@ use super::common::{ async fn test_snat_crud(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; + let (mut client, _) = make_test_client(pool).await; // admin login - authenticate_admin(&client).await; + authenticate_admin(&mut client).await; // create location let response = client @@ -110,10 +110,10 @@ async fn test_snat_crud(_: PgPoolOptions, options: PgConnectOptions) { async fn test_snat_enterprise_required(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _) = make_test_client(pool).await; + let (mut client, _) = make_test_client(pool).await; // admin login - authenticate_admin(&client).await; + authenticate_admin(&mut client).await; exceed_enterprise_limits(&client).await; diff --git a/crates/defguard_core/tests/integration/api/user.rs b/crates/defguard_core/tests/integration/api/user.rs index d7605a5348..1028c6093e 100644 --- a/crates/defguard_core/tests/integration/api/user.rs +++ b/crates/defguard_core/tests/integration/api/user.rs @@ -4,6 +4,7 @@ use defguard_core::{ AddDevice, UserInfo, models::{NewOpenIDClient, oauth2client::OAuth2Client}, }, + events::ApiEventType, handlers::{AddUserData, Auth, PasswordChange, PasswordChangeSelf, Username}, }; use reqwest::{StatusCode, header::USER_AGENT}; @@ -14,12 +15,13 @@ use super::{ TEST_SERVER_URL, common::{fetch_user_details, make_client, make_network, make_test_client, setup_pool}, }; +use crate::api::common::{get_db_device, get_db_location, get_db_user, make_client_with_db}; #[sqlx::test] async fn test_authenticate(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; @@ -32,35 +34,44 @@ async fn test_authenticate(_: PgPoolOptions, options: PgConnectOptions) { let auth = Auth::new("adumbledore", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); + + // second user does not exist so we are unable to emit audit log event + client.verify_api_events_with_user(&[ + (ApiEventType::UserLogin, 2, "hpotter"), + ( + ApiEventType::UserLoginFailed { + message: "Authentication for hpotter failed: invalid password".into(), + }, + 2, + "hpotter", + ), + ]); } #[sqlx::test] async fn test_me(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; - let auth = Auth::new("hpotter", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("hpotter", "pass123").await; let response = client.get("/api/v1/me").send().await; assert_eq!(response.status(), StatusCode::OK); let user_info: UserInfo = response.json().await; assert_eq!(user_info.first_name, "Harry"); assert_eq!(user_info.last_name, "Potter"); + + client.assert_event_queue_is_empty(); } #[sqlx::test] async fn test_change_self_password(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; - let auth = Auth::new("hpotter", "pass123"); - - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("hpotter", "pass123").await; let bad_old = "notCurrentPassword123!$"; @@ -103,6 +114,7 @@ async fn test_change_self_password(_: PgPoolOptions, options: PgConnectOptions) assert_eq!(response.status(), StatusCode::OK); // old pass login + let auth = Auth::new("hpotter", "pass123"); let response = client.post("/api/v1/auth").json(&auth).send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); @@ -110,18 +122,27 @@ async fn test_change_self_password(_: PgPoolOptions, options: PgConnectOptions) let response = client.post("/api/v1/auth").json(&new_auth).send().await; assert_eq!(response.status(), StatusCode::OK); + + client.verify_api_events_with_user(&[ + (ApiEventType::PasswordChanged, 2, "hpotter"), + ( + ApiEventType::UserLoginFailed { + message: "Authentication for hpotter failed: invalid password".into(), + }, + 2, + "hpotter", + ), + (ApiEventType::UserLogin, 2, "hpotter"), + ]); } #[sqlx::test] async fn test_change_password(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; let new_password = "newPassword43$!"; @@ -159,62 +180,69 @@ async fn test_change_password(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::FORBIDDEN); + + let test_user = get_db_user(&pool, "hpotter").await; + + client.verify_api_events_with_user(&[ + ( + ApiEventType::PasswordChangedByAdmin { user: test_user }, + 1, + "admin", + ), + (ApiEventType::UserLogin, 2, "hpotter"), + ]); } #[sqlx::test] async fn test_list_users(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; let response = client.get("/api/v1/user").send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // normal user cannot list users - let auth = Auth::new("hpotter", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("hpotter", "pass123").await; let response = client.get("/api/v1/user").send().await; assert_eq!(response.status(), StatusCode::FORBIDDEN); // admin can list users - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; let response = client.get("/api/v1/user").send().await; assert_eq!(response.status(), StatusCode::OK); + + client.assert_event_queue_is_empty(); } #[sqlx::test] async fn test_get_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; let response = client.get("/api/v1/user/hpotter").send().await; assert_eq!(response.status(), StatusCode::UNAUTHORIZED); - let auth = Auth::new("hpotter", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("hpotter", "pass123").await; let user_info = fetch_user_details(&client, "hpotter").await; assert_eq!(user_info.user.first_name, "Harry"); assert_eq!(user_info.user.last_name, "Potter"); + + client.assert_event_queue_is_empty(); } #[sqlx::test] async fn test_username_available(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let mut client = make_client(pool).await; // standard user cannot check username availability - let auth = Auth::new("hpotter", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("hpotter", "pass123").await; let avail = Username { username: "hpotter".into(), @@ -227,9 +255,7 @@ async fn test_username_available(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::FORBIDDEN); // log in as admin - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; let avail = Username { username: "_CrashTestDummy".into(), @@ -260,17 +286,17 @@ async fn test_username_available(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + client.assert_event_queue_is_empty(); } #[sqlx::test] async fn test_crud_user(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; // create user let new_user = AddUserData { @@ -288,6 +314,8 @@ async fn test_crud_user(_: PgPoolOptions, options: PgConnectOptions) { let mut user_details = fetch_user_details(&client, "adumbledore").await; assert_eq!(user_details.user.first_name, "Albus"); + let old_test_user = get_db_user(&pool, "adumbledore").await; + // edit user user_details.user.phone = Some("5678".into()); let response = client @@ -297,20 +325,33 @@ async fn test_crud_user(_: PgPoolOptions, options: PgConnectOptions) { .await; assert_eq!(response.status(), StatusCode::OK); + let new_test_user = get_db_user(&pool, "adumbledore").await; + // delete user let response = client.delete("/api/v1/user/adumbledore").send().await; assert_eq!(response.status(), StatusCode::OK); + + client.verify_api_events(&[ + ApiEventType::UserAdded { + user: old_test_user.clone(), + }, + ApiEventType::UserModified { + before: old_test_user, + after: new_test_user.clone(), + }, + ApiEventType::UserRemoved { + user: new_test_user, + }, + ]); } #[sqlx::test] async fn test_check_username(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; let invalid_usernames = ["ADumble dore", ".1user"]; let valid_usernames = ["user1", "use2r3", "not_wrong"]; @@ -328,6 +369,7 @@ async fn test_check_username(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(response.status(), StatusCode::BAD_REQUEST); } + let mut expected_events = Vec::new(); for (i, username) in valid_usernames.into_iter().enumerate() { let new_user = AddUserData { username: username.into(), @@ -339,19 +381,22 @@ async fn test_check_username(_: PgPoolOptions, options: PgConnectOptions) { }; let response = client.post("/api/v1/user").json(&new_user).send().await; assert_eq!(response.status(), StatusCode::CREATED); + + let test_user = get_db_user(&pool, username).await; + expected_events.push(ApiEventType::UserAdded { user: test_user }) } + + client.verify_api_events(&expected_events); } #[sqlx::test] async fn test_check_password_strength(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; // auth session with admin - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; // test let strong_password = "strongPass1234$!"; @@ -391,16 +436,20 @@ async fn test_check_password_strength(_: PgPoolOptions, options: PgConnectOption .send() .await; assert_eq!(response.status(), StatusCode::CREATED); + + let test_user = get_db_user(&pool, "strongpass").await; + + client.verify_api_events(&[ApiEventType::UserAdded { user: test_user }]); } #[sqlx::test] async fn test_user_unregister_authorized_app(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + let (mut client, pool) = make_client_with_db(pool).await; + client.login_user("admin", "pass123").await; + + // add OpenID app let openid_client = NewOpenIDClient { name: "Test".into(), redirect_uri: vec![TEST_SERVER_URL.into()], @@ -415,6 +464,13 @@ async fn test_user_unregister_authorized_app(_: PgPoolOptions, options: PgConnec assert_eq!(response.status(), StatusCode::CREATED); let openid_client: OAuth2Client = response.json().await; assert_eq!(openid_client.name, "Test"); + + // verify app is not authorized yet + let response = client.get("/api/v1/me").send().await; + let user_info: UserInfo = response.json().await; + assert_eq!(user_info.authorized_apps.len(), 0); + + // authorize app let response = client .post(format!( "/api/v1/oauth/authorize?\ @@ -433,6 +489,10 @@ async fn test_user_unregister_authorized_app(_: PgPoolOptions, options: PgConnec let response = client.get("/api/v1/me").send().await; let mut user_info: UserInfo = response.json().await; assert_eq!(user_info.authorized_apps.len(), 1); + + let old_test_user = get_db_user(&pool, "admin").await; + + // unregister app user_info.authorized_apps = [].into(); let response = client .put("/api/v1/user/admin") @@ -443,16 +503,28 @@ async fn test_user_unregister_authorized_app(_: PgPoolOptions, options: PgConnec let response = client.get("/api/v1/me").send().await; let user_info: UserInfo = response.json().await; assert_eq!(user_info.authorized_apps.len(), 0); + + let new_test_user = get_db_user(&pool, "admin").await; + + client.verify_api_events(&[ + ApiEventType::OpenIdAppAdded { app: openid_client }, + ApiEventType::UserModified { + before: old_test_user, + after: new_test_user.clone(), + }, + ]); } #[sqlx::test] async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, state) = make_test_client(pool).await; + let (mut client, state) = make_test_client(pool).await; let mut mail_rx = state.mail_rx; let user_agent_header = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1"; + let mut expected_events = Vec::new(); + // log in as admin let auth = Auth::new("admin", "pass123"); let response = client @@ -462,6 +534,7 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::OK); + expected_events.push(ApiEventType::UserLogin); // first email received is regarding admin login let mail = mail_rx.try_recv().unwrap(); @@ -478,6 +551,9 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::CREATED); + expected_events.push(ApiEventType::VpnLocationAdded { + location: get_db_location(&state.pool, 1).await, + }); // add device for user let device_data = AddDevice { @@ -491,6 +567,10 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::CREATED); + expected_events.push(ApiEventType::UserDeviceAdded { + owner: get_db_user(&state.pool, "hpotter").await, + device: get_db_device(&state.pool, 1).await, + }); // send email regarding new device being added // it does not contain session info @@ -512,6 +592,10 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::CREATED); + expected_events.push(ApiEventType::UserDeviceAdded { + owner: get_db_user(&state.pool, "admin").await, + device: get_db_device(&state.pool, 2).await, + }); // send email regarding new device being added // it should contain session info @@ -533,6 +617,7 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::OK); + expected_events.push(ApiEventType::UserLogin); let response = client.get("/api/v1/me").send().await; assert_eq!(response.status(), StatusCode::OK); @@ -580,6 +665,10 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { .send() .await; assert_eq!(response.status(), StatusCode::CREATED); + expected_events.push(ApiEventType::UserDeviceAdded { + owner: get_db_user(&state.pool, "hpotter").await, + device: get_db_device(&state.pool, 3).await, + }); // send email regarding new device being added let mail = mail_rx.try_recv().unwrap(); @@ -590,17 +679,17 @@ async fn test_user_add_device(_: PgPoolOptions, options: PgConnectOptions) { mail.content .contains("Device type: iPhone, OS: iOS 17.1, Mobile Safari") ); + + client.verify_api_events(&expected_events); } #[sqlx::test] async fn test_disable(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; // get yourself let mut user_details = fetch_user_details(&client, "admin").await; @@ -632,6 +721,8 @@ async fn test_disable(_: PgPoolOptions, options: PgConnectOptions) { assert_eq!(user_details.user.first_name, "Albus"); assert!(user_details.user.is_active); + let old_test_user = get_db_user(&pool, "adumbledore").await; + // disable user user_details.user.is_active = false; let response = client @@ -644,17 +735,27 @@ async fn test_disable(_: PgPoolOptions, options: PgConnectOptions) { let user_details = fetch_user_details(&client, "adumbledore").await; assert_eq!(user_details.user.first_name, "Albus"); assert!(!user_details.user.is_active); + + let new_test_user = get_db_user(&pool, "adumbledore").await; + + client.verify_api_events(&[ + ApiEventType::UserAdded { + user: old_test_user.clone(), + }, + ApiEventType::UserModified { + before: old_test_user, + after: new_test_user.clone(), + }, + ]); } #[sqlx::test] async fn test_unique_email(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let client = make_client(pool).await; + let (mut client, pool) = make_client_with_db(pool).await; - let auth = Auth::new("admin", "pass123"); - let response = client.post("/api/v1/auth").json(&auth).send().await; - assert_eq!(response.status(), StatusCode::OK); + client.login_user("admin", "pass123").await; // create user let new_user = AddUserData { @@ -679,4 +780,8 @@ async fn test_unique_email(_: PgPoolOptions, options: PgConnectOptions) { }; let response = client.post("/api/v1/user").json(&new_user).send().await; assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let test_user = get_db_user(&pool, "adumbledore").await; + + client.verify_api_events(&[ApiEventType::UserAdded { user: test_user }]); } diff --git a/crates/defguard_core/tests/integration/api/wireguard.rs b/crates/defguard_core/tests/integration/api/wireguard.rs index 79625944c0..36c5ac4e2b 100644 --- a/crates/defguard_core/tests/integration/api/wireguard.rs +++ b/crates/defguard_core/tests/integration/api/wireguard.rs @@ -8,6 +8,7 @@ use defguard_core::{ device::WireguardNetworkDevice, wireguard::{ DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode, + ServiceLocationMode, }, }, }, @@ -71,6 +72,7 @@ async fn test_network(_: PgPoolOptions, options: PgConnectOptions) { acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode::Disabled, + service_location_mode: ServiceLocationMode::Disabled, }; let response = client .put(format!("/api/v1/network/{}", network.id)) @@ -128,8 +130,8 @@ async fn test_network(_: PgPoolOptions, options: PgConnectOptions) { async fn test_location_mfa_mode_validation_create(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _client_state) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _client_state) = make_test_client(pool).await; + authenticate_admin(&mut client).await; exceed_enterprise_limits(&client).await; @@ -150,6 +152,7 @@ async fn test_location_mfa_mode_validation_create(_: PgPoolOptions, options: PgC acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode::External, + service_location_mode: ServiceLocationMode::Disabled, }; // create network @@ -190,6 +193,7 @@ async fn test_location_mfa_mode_validation_create(_: PgPoolOptions, options: PgC directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client @@ -213,8 +217,8 @@ async fn test_location_mfa_mode_validation_create(_: PgPoolOptions, options: PgC async fn test_location_mfa_mode_validation_modify(_: PgPoolOptions, options: PgConnectOptions) { let pool = setup_pool(options).await; - let (client, _client_state) = make_test_client(pool).await; - authenticate_admin(&client).await; + let (mut client, _client_state) = make_test_client(pool).await; + authenticate_admin(&mut client).await; let mut location_data = WireguardNetworkData { name: "test_location".into(), @@ -229,6 +233,7 @@ async fn test_location_mfa_mode_validation_modify(_: PgPoolOptions, options: PgC acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode::Disabled, + service_location_mode: ServiceLocationMode::Disabled, }; // create network @@ -284,6 +289,7 @@ async fn test_location_mfa_mode_validation_modify(_: PgPoolOptions, options: PgC directory_sync_group_match: None, username_handling: OpenidUsernameHandling::PruneEmailDomain, jumpcloud_api_key: None, + prefetch_users: false, }; let response = client @@ -489,7 +495,8 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }); let response = client.post("/api/v1/network").json(&network).send().await; assert_eq!(response.status(), StatusCode::CREATED); @@ -557,7 +564,8 @@ async fn test_network_address_reassignment(_: PgPoolOptions, options: PgConnectO "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }); let response = client .put(format!("/api/v1/network/{}", network_from_details.id)) @@ -832,3 +840,142 @@ async fn test_device_pubkey(_: PgPoolOptions, options: PgConnectOptions) { let devices: Vec> = response.json().await; assert_eq!(devices.len(), 1); } + +#[sqlx::test] +async fn test_network_size_validation(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + + let (client, _client_state) = make_test_client(pool).await; + + let auth = Auth::new("admin", "pass123"); + let response = &client.post("/api/v1/auth").json(&auth).send().await; + assert_eq!(response.status(), StatusCode::OK); + + // create network + let network = json!({ + "name": "network", + "address": "10.1.1.1/24", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "allowed_groups": [], + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + }); + let response = client.post("/api/v1/network").json(&network).send().await; + assert_eq!(response.status(), StatusCode::CREATED); + + // network details + let response = client.get("/api/v1/network/1").send().await; + assert_eq!(response.status(), StatusCode::OK); + let network_from_details: WireguardNetwork = response.json().await; + + // create devices + let device = json!({ + "name": "device1", + "wireguard_pubkey": "LQKsT6/3HWKuJmMulH63R8iK+5sI8FyYEL6WDIi6lQU=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let device = json!({ + "name": "device2", + "wireguard_pubkey": "ZqDlG4LQZRO9v57Sd27AHdtTLxegbMp5oVThjYrg21I=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + let device = json!({ + "name": "device3", + "wireguard_pubkey": "o/8q3kmv5nnbrcb/7aceQWGE44a0yI707wObXRyyWGU=", + }); + let response = client + .post("/api/v1/device/admin") + .json(&device) + .send() + .await; + assert_eq!(response.status(), StatusCode::CREATED); + + // try to add subnet with not enough IPs + let network = json!({ + "id": network_from_details.id, + "name": "network", + "address": "10.1.1.1/24,10.2.1.1/30", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "allowed_groups": [], + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + }); + let response = client + .put(format!("/api/v1/network/{}", network_from_details.id)) + .json(&network) + .send() + .await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // try to add subnet with invalid mask + let network = json!({ + "id": network_from_details.id, + "name": "network", + "address": "10.2.0.1/24,10.1.1.1/0", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "allowed_groups": [], + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + }); + let response = client + .put(format!("/api/v1/network/{}", network_from_details.id)) + .json(&network) + .send() + .await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // try to add no network + let network = json!({ + "id": network_from_details.id, + "name": "network", + "address": "", + "port": 55555, + "endpoint": "192.168.4.14", + "allowed_ips": "10.1.1.0/24", + "dns": "1.1.1.1", + "allowed_groups": [], + "keepalive_interval": 25, + "peer_disconnect_threshold": 300, + "acl_enabled": false, + "acl_default_allow": false, + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" + }); + let response = client + .put(format!("/api/v1/network/{}", network_from_details.id)) + .json(&network) + .send() + .await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); +} diff --git a/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs b/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs index 3a2f9e15a1..d641de589e 100644 --- a/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_allowed_groups.rs @@ -151,7 +151,8 @@ async fn test_create_new_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -197,7 +198,8 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -230,7 +232,8 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -257,7 +260,8 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -285,7 +289,8 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -312,7 +317,8 @@ async fn test_modify_network(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -561,7 +567,8 @@ async fn test_modify_user(_: PgPoolOptions, options: PgConnectOptions) { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; @@ -660,7 +667,8 @@ async fn test_delete_only_allowed_group(_: PgPoolOptions, options: PgConnectOpti "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" })) .send() .await; diff --git a/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs b/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs index f16ed592bd..225df95bb2 100644 --- a/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_devices.rs @@ -27,7 +27,8 @@ fn make_network() -> Value { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }) } @@ -44,7 +45,8 @@ fn make_second_network() -> Value { "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }) } @@ -303,7 +305,8 @@ async fn test_device_ip_validation(_: PgPoolOptions, options: PgConnectOptions) "peer_disconnect_threshold": 300, "acl_enabled": false, "acl_default_allow": false, - "location_mfa_mode": "disabled" + "location_mfa_mode": "disabled", + "service_location_mode": "disabled" }); let response = client.post("/api/v1/network").json(&location).send().await; assert_eq!(response.status(), StatusCode::CREATED); diff --git a/crates/defguard_core/tests/integration/api/wireguard_network_import.rs b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs index aad4c8d62c..9db3ce63eb 100644 --- a/crates/defguard_core/tests/integration/api/wireguard_network_import.rs +++ b/crates/defguard_core/tests/integration/api/wireguard_network_import.rs @@ -7,6 +7,7 @@ use defguard_core::{ device::{DeviceType, UserDevice}, wireguard::{ DEFAULT_DISCONNECT_THRESHOLD, DEFAULT_KEEPALIVE_INTERVAL, LocationMfaMode, + ServiceLocationMode, }, }, }, @@ -62,6 +63,7 @@ async fn test_config_import(_: PgPoolOptions, options: PgConnectOptions) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ); initial_network.save(&pool).await.unwrap(); diff --git a/crates/defguard_core/tests/integration/grpc/gateway.rs b/crates/defguard_core/tests/integration/grpc/gateway.rs index 8be4c88c31..d27fca1e72 100644 --- a/crates/defguard_core/tests/integration/grpc/gateway.rs +++ b/crates/defguard_core/tests/integration/grpc/gateway.rs @@ -10,7 +10,8 @@ use defguard_core::{ db::{ Device, User, WireguardNetwork, models::{ - device::DeviceType, wireguard::LocationMfaMode, + device::DeviceType, + wireguard::{LocationMfaMode, ServiceLocationMode}, wireguard_peer_stats::WireguardPeerStats, }, }, @@ -50,6 +51,7 @@ async fn setup_test_server( false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -399,6 +401,7 @@ async fn test_gateway_update_routing(_: PgPoolOptions, options: PgConnectOptions false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -520,6 +523,7 @@ async fn test_gateway_config(_: PgPoolOptions, options: PgConnectOptions) { false, false, LocationMfaMode::Disabled, + ServiceLocationMode::Disabled, ) .save(&pool) .await @@ -554,3 +558,117 @@ async fn test_gateway_version_validation(_: PgPoolOptions, options: PgConnectOpt let status = response.err().unwrap(); assert_eq!(status.code(), Code::FailedPrecondition); } + +// https://github.com/DefGuard/defguard/issues/1671 +#[sqlx::test] +async fn test_device_pubkey_change(_: PgPoolOptions, options: PgConnectOptions) { + let pool = setup_pool(options).await; + let (mut test_server, mut gateway, test_location, test_user) = + setup_test_server(pool.clone()).await; + + // initial client map is empty + { + let client_map = test_server.get_client_map(); + assert!(client_map.is_empty()) + } + + // connect stats stream + let stats_tx = gateway.setup_stats_update_stream().await; + let mut update_id = 1; + + // add user device + let device_pubkey = "wYOt6ImBaQ3BEMQ3Xf5P5fTnbqwOvjcqYkkSBt+1xOg="; + let mut test_device = Device::new( + "test device".into(), + device_pubkey.into(), + test_user.id, + DeviceType::User, + None, + true, + ) + .save(&pool) + .await + .unwrap(); + + // send stats update for existing device + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: Utc::now().timestamp() as u64, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + // wait for event to be emitted + sleep(Duration::from_millis(100)).await; + let grpc_event = test_server + .grpc_event_rx + .try_recv() + .expect("failed to receive gRPC event"); + assert_matches!( + grpc_event, + GrpcEvent::ClientConnected { + context: _, + location, + device + } if ((location.id == test_location.id) & (device.id == test_device.id)) + ); + + // change device pubkey + let new_device_pubkey = "TJG2T6rhndZtk06KnIIOlD6hhd7wpVkBss8sfyvMCAA="; + test_device.wireguard_pubkey = new_device_pubkey.to_owned(); + test_device.save(&pool).await.unwrap(); + + // send stats update with old pubkey + update_id += 1; + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: Utc::now().timestamp() as u64, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + // no event should be emitted + sleep(Duration::from_millis(100)).await; + assert_err_eq!(test_server.grpc_event_rx.try_recv(), TryRecvError::Empty); + + // send stats update with new pubkey + update_id += 1; + stats_tx + .send(StatsUpdate { + id: update_id, + payload: Some(Payload::PeerStats(PeerStats { + public_key: new_device_pubkey.into(), + endpoint: "1.2.3.4:1234".into(), + latest_handshake: Utc::now().timestamp() as u64, + ..Default::default() + })), + }) + .expect("failed to send stats update"); + + // wait for event + // FIXME: ideally this should not be emitted; we'll fix it once we implement a more robust VPN session logic + sleep(Duration::from_millis(100)).await; + let grpc_event = test_server + .grpc_event_rx + .try_recv() + .expect("failed to receive gRPC event"); + + assert_matches!( + grpc_event, + GrpcEvent::ClientConnected { + context: _, + location, + device + } if ((location.id == test_location.id) & (device.id == test_device.id)) + ); +} diff --git a/crates/defguard_proto/src/lib.rs b/crates/defguard_proto/src/lib.rs index b7a38faaaf..0313436669 100644 --- a/crates/defguard_proto/src/lib.rs +++ b/crates/defguard_proto/src/lib.rs @@ -29,11 +29,11 @@ impl fmt::Display for MfaMethod { f, "{}", match self { - MfaMethod::Totp => "TOTP", - MfaMethod::Email => "Email", - MfaMethod::Oidc => "OIDC", - MfaMethod::Biometric => "Biometric", - MfaMethod::MobileApprove => "MobileApprove", + Self::Totp => "TOTP", + Self::Email => "Email", + Self::Oidc => "OIDC", + Self::Biometric => "Biometric", + Self::MobileApprove => "MobileApprove", } ) } @@ -45,11 +45,11 @@ impl Serialize for MfaMethod { S: serde::Serializer, { match *self { - MfaMethod::Totp => serializer.serialize_unit_variant("MfaMethod", 0, "Totp"), - MfaMethod::Email => serializer.serialize_unit_variant("MfaMethod", 1, "Email"), - MfaMethod::Oidc => serializer.serialize_unit_variant("MfaMethod", 2, "Oidc"), - MfaMethod::Biometric => serializer.serialize_unit_variant("MfaMethod", 3, "Biometric"), - MfaMethod::MobileApprove => { + Self::Totp => serializer.serialize_unit_variant("MfaMethod", 0, "Totp"), + Self::Email => serializer.serialize_unit_variant("MfaMethod", 1, "Email"), + Self::Oidc => serializer.serialize_unit_variant("MfaMethod", 2, "Oidc"), + Self::Biometric => serializer.serialize_unit_variant("MfaMethod", 3, "Biometric"), + Self::MobileApprove => { serializer.serialize_unit_variant("MfaMethod", 4, "MobileApprove") } } diff --git a/crates/defguard_version/src/lib.rs b/crates/defguard_version/src/lib.rs index 18c19a1339..05f177b24f 100644 --- a/crates/defguard_version/src/lib.rs +++ b/crates/defguard_version/src/lib.rs @@ -103,9 +103,9 @@ impl FromStr for DefguardComponent { fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { - "core" => Ok(DefguardComponent::Core), - "proxy" => Ok(DefguardComponent::Proxy), - "gateway" => Ok(DefguardComponent::Gateway), + "core" => Ok(Self::Core), + "proxy" => Ok(Self::Proxy), + "gateway" => Ok(Self::Gateway), _ => Err(Self::Err::InvalidDefguardComponent(s.to_string())), } } @@ -217,7 +217,7 @@ pub struct ComponentInfo { } impl ComponentInfo { - /// Creates a new ComponentInfo with the provided version and automatically detects + /// Creates a new `ComponentInfo` with the provided version and automatically detects /// the current system information. /// /// # Arguments diff --git a/crates/defguard_version/src/tracing.rs b/crates/defguard_version/src/tracing.rs index c81793499b..bcf33fecdf 100644 --- a/crates/defguard_version/src/tracing.rs +++ b/crates/defguard_version/src/tracing.rs @@ -209,8 +209,8 @@ pub fn build_version_suffix( /// Custom tracing formatter that conditionally includes version information in log messages. /// /// This formatter wraps the default tracing formatter and adds version suffix to log messages: -/// - For ERROR level logs: includes own_version, own_info and components version and info -/// - For other levels: includes only own_version and component version if available +/// - For ERROR level logs: includes `own_version`, `own_info` and components version and info +/// - For other levels: includes only `own_version` and component version if available /// /// The version information is extracted from tracing span fields. pub struct VersionSuffixFormat { diff --git a/deny.toml b/deny.toml index d26808d382..49991da9e5 100644 --- a/deny.toml +++ b/deny.toml @@ -71,7 +71,7 @@ feature-depth = 1 # output a note when they are encountered. ignore = [ { id = "RUSTSEC-2023-0071", reason = "https://github.com/RustCrypto/RSA/issues/19" }, - { id = "RUSTSEC-2024-0436", reason = "Unmaintained" }, + { id = "RUSTSEC-2024-0436", reason = "Unmaintained dependency of tera" }, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -89,6 +89,7 @@ ignore = [ allow = [ "MIT", "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", "MPL-2.0", "BSD-3-Clause", "Unicode-3.0", @@ -109,34 +110,34 @@ confidence-threshold = 0.8 # aren't accepted for every possible crate as with the normal allow list exceptions = [ { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_common" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_core" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_mail" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_proto" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_web_ui" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_event_router" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_event_logger" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "defguard_version" }, { allow = [ - "AGPL-3.0-only", + "AGPL-3.0-only", "AGPL-3.0-or-later", ], crate = "model_derive" }, ] diff --git a/docker-compose.e2e.yaml b/docker-compose.e2e.yaml index 7009e3ee17..c3fa1d0393 100644 --- a/docker-compose.e2e.yaml +++ b/docker-compose.e2e.yaml @@ -24,7 +24,7 @@ services: - db db: - image: postgres:17-alpine + image: public.ecr.aws/docker/library/postgres:17-alpine environment: POSTGRES_DB: defguard POSTGRES_USER: defguard diff --git a/e2e/tests/vpn/wizard.spec.ts b/e2e/tests/vpn/wizard.spec.ts index 1f9f8c48cd..bd6756cc6e 100644 --- a/e2e/tests/vpn/wizard.spec.ts +++ b/e2e/tests/vpn/wizard.spec.ts @@ -44,7 +44,7 @@ test.describe('Setup VPN (wizard) ', () => { await page.getByTestId('setup-option-import').click(); await navNext.click(); await page.getByTestId('field-name').fill('test network'); - await page.getByTestId('field-endpoint').fill('127.0.0.1:5051'); + await page.getByTestId('field-endpoint').fill('127.0.0.1'); const fileChooserPromise = page.waitForEvent('filechooser'); await page.getByTestId('upload-config').click(); const responseImportConfigPromise = page.waitForResponse('**/import'); diff --git a/flake.lock b/flake.lock index 6b2f9bdad5..8f18766cd4 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1758035966, - "narHash": "sha256-qqIJ3yxPiB0ZQTT9//nFGQYn8X/PBoJbofA7hRKZnmE=", + "lastModified": 1763966396, + "narHash": "sha256-6eeL1YPcY1MV3DDStIDIdy/zZCDKgHdkCmsrLJFiZf0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8d4ddb19d03c65a36ad8d189d001dc32ffb0306b", + "rev": "5ae3b07d8d6527c42f17c876e404993199144b6a", "type": "github" }, "original": { @@ -48,11 +48,11 @@ ] }, "locked": { - "lastModified": 1758204348, - "narHash": "sha256-jkz/NihbcEwy1EHDv/6g0HEqkpyIWCnQ1siGrhHEtFM=", + "lastModified": 1764124769, + "narHash": "sha256-vcoOEy3i8AGJi3Y2C48hrf6CuL2h8W1gLe1gNt72Kxg=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "067b3536e55341f579385ce8593cdcc9d022972b", + "rev": "5da8c00313b4434f00aed6b4c94cd3b207bafdc5", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index f2e6a984c1..1ee1a20d86 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,7 @@ # Specify the rust-src path (many editors rely on this) RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; + PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS = true; }; }); } diff --git a/images/ami/core.pkr.hcl b/images/ami/core.pkr.hcl deleted file mode 100644 index 477cfea388..0000000000 --- a/images/ami/core.pkr.hcl +++ /dev/null @@ -1,62 +0,0 @@ -packer { - required_plugins { - amazon = { - version = ">= 1.2.8" - source = "github.com/hashicorp/amazon" - } - } -} - -variable "package_version" { - type = string -} - -variable "region" { - type = string - default = "eu-north-1" -} - -variable "instance_type" { - type = string - default = "t3.micro" -} - -source "amazon-ebs" "defguard-core" { - ami_name = "defguard-core-${var.package_version}-amd64" - instance_type = var.instance_type - region = var.region - source_ami_filter { - filters = { - name = "debian-13-amd64-*" - root-device-type = "ebs" - virtualization-type = "hvm" - } - most_recent = true - owners = ["136693071363"] - } - ssh_username = "admin" -} - -build { - name = "defguard-core" - sources = [ - "source.amazon-ebs.defguard-core" - ] - - provisioner "file" { - source = "defguard-${var.package_version}-x86_64-unknown-linux-gnu.deb" - destination = "/tmp/defguard-core.deb" - } - - provisioner "shell" { - script = "./images/ami/core.sh" - } - - provisioner "shell" { - inline = ["rm /home/admin/.ssh/authorized_keys"] - } - - provisioner "shell" { - inline = ["sudo rm /root/.ssh/authorized_keys"] - } -} diff --git a/images/ami/core.sh b/images/ami/core.sh deleted file mode 100644 index 1203c711ae..0000000000 --- a/images/ami/core.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash -set -e - -echo "Updating apt repositories..." -sudo apt update - -echo "Installing Defguard package..." -sudo dpkg -i /tmp/defguard-core.deb - -echo "Cleaning up..." -sudo rm -f /tmp/defguard-core.deb - -echo "Defguard installation completed successfully." diff --git a/migrations/20251015080719_service_locations.down.sql b/migrations/20251015080719_service_locations.down.sql new file mode 100644 index 0000000000..2cc3003aa7 --- /dev/null +++ b/migrations/20251015080719_service_locations.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE wireguard_network DROP COLUMN "service_location_mode"; +DROP TYPE service_location_mode; diff --git a/migrations/20251015080719_service_locations.up.sql b/migrations/20251015080719_service_locations.up.sql new file mode 100644 index 0000000000..4347d98b12 --- /dev/null +++ b/migrations/20251015080719_service_locations.up.sql @@ -0,0 +1,7 @@ +CREATE TYPE service_location_mode AS ENUM ( + 'disabled', + 'prelogon', + 'alwayson' +); + +ALTER TABLE wireguard_network ADD COLUMN "service_location_mode" service_location_mode NOT NULL DEFAULT 'disabled'; diff --git a/migrations/20251030081542_user_enrollment_pending.down.sql b/migrations/20251030081542_user_enrollment_pending.down.sql new file mode 100644 index 0000000000..2354958309 --- /dev/null +++ b/migrations/20251030081542_user_enrollment_pending.down.sql @@ -0,0 +1 @@ +ALTER TABLE "user" DROP COLUMN enrollment_pending; diff --git a/migrations/20251030081542_user_enrollment_pending.up.sql b/migrations/20251030081542_user_enrollment_pending.up.sql new file mode 100644 index 0000000000..e445c24d8b --- /dev/null +++ b/migrations/20251030081542_user_enrollment_pending.up.sql @@ -0,0 +1 @@ +ALTER TABLE "user" ADD COLUMN enrollment_pending BOOLEAN NOT NULL DEFAULT false; diff --git a/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql b/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql new file mode 100644 index 0000000000..84539f0ec4 --- /dev/null +++ b/migrations/20251103105138_openid_directory_sync_prefetch_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider DROP COLUMN prefetch_users; diff --git a/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql b/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql new file mode 100644 index 0000000000..5e93a274fd --- /dev/null +++ b/migrations/20251103105138_openid_directory_sync_prefetch_users.up.sql @@ -0,0 +1 @@ +ALTER TABLE openidprovider ADD COLUMN prefetch_users BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/20251119122424_client_traffic_policy.down.sql b/migrations/20251119122424_client_traffic_policy.down.sql new file mode 100644 index 0000000000..db730678d6 --- /dev/null +++ b/migrations/20251119122424_client_traffic_policy.down.sql @@ -0,0 +1,13 @@ +-- restore boolean `mfa_enabled` column +ALTER TABLE enterprisesettings ADD COLUMN "disable_all_traffic" BOOLEAN NOT NULL DEFAULT false; + +-- populate based on client traffic policy +UPDATE enterprisesettings +SET disable_all_traffic = CASE + WHEN client_traffic_policy = 'disable_all_traffic'::client_traffic_policy THEN true + ELSE false +END; + +-- drop new column and type +ALTER TABLE enterprisesettings DROP COLUMN "client_traffic_policy"; +DROP TYPE client_traffic_policy; diff --git a/migrations/20251119122424_client_traffic_policy.up.sql b/migrations/20251119122424_client_traffic_policy.up.sql new file mode 100644 index 0000000000..a5bfb8d8ac --- /dev/null +++ b/migrations/20251119122424_client_traffic_policy.up.sql @@ -0,0 +1,19 @@ +-- add enum representing client traffic policy +CREATE TYPE client_traffic_policy AS ENUM ( + 'none', + 'disable_all_traffic', + 'force_all_traffic' +); + +-- add column to `enterprisesettings` table +ALTER TABLE enterprisesettings ADD COLUMN "client_traffic_policy" client_traffic_policy NOT NULL DEFAULT 'none'; + +-- populate new column based on value in `disable_all_traffic` column +UPDATE enterprisesettings +SET client_traffic_policy = CASE + WHEN disable_all_traffic = true THEN 'disable_all_traffic'::client_traffic_policy + ELSE 'none'::client_traffic_policy +END; + +-- drop the `disable_all_traffic` column since it's no longer needed +ALTER TABLE enterprisesettings DROP COLUMN "disable_all_traffic"; diff --git a/proto b/proto index 883487df67..5dfc8c8d23 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 883487df67d90fd14fae900737cd8b5ea6c10de3 +Subproject commit 5dfc8c8d23ac0613108a2b7b921fd9a97613bb3a diff --git a/web/package.json b/web/package.json index 72f2c3acba..0311645409 100644 --- a/web/package.json +++ b/web/package.json @@ -14,7 +14,10 @@ "typesafe-i18n": "typesafe-i18n", "vite": "vite", "prettier": "prettier", - "biome": "biome" + "biome": "biome", + "test": "vitest run", + "test:ui": "vitest --ui", + "test:watch": "vitest" }, "browserslist": { "production": [ @@ -40,7 +43,10 @@ ], "onlyBuiltDependencies": [ "@swc/core" - ] + ], + "overrides": { + "mdast-util-to-hast": "13.2.1" + } }, "dependencies": { "@floating-ui/react": "^0.27.16", @@ -50,17 +56,17 @@ "@react-rxjs/core": "^0.10.8", "@stablelib/base64": "^2.0.1", "@stablelib/x25519": "^2.0.1", - "@tanstack/query-core": "^5.90.2", - "@tanstack/react-query": "^5.90.2", + "@tanstack/query-core": "^5.90.9", + "@tanstack/react-query": "^5.90.9", "@tanstack/react-virtual": "3.13.12", "@tanstack/virtual-core": "3.13.12", "@use-gesture/react": "^10.3.1", - "axios": "^1.12.2", + "axios": "^1.13.2", "byte-size": "^9.0.1", "classnames": "^2.5.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", - "dayjs": "^1.11.18", + "dayjs": "^1.11.19", "deepmerge-ts": "^7.1.5", "detect-browser": "^5.3.0", "dice-coefficient": "^2.1.1", @@ -70,7 +76,7 @@ "fuse.js": "^7.1.0", "get-text-width": "^1.0.3", "hex-rgb": "^5.0.0", - "html-react-parser": "^5.2.6", + "html-react-parser": "^5.2.8", "humanize-duration": "^3.33.1", "ipaddr.js": "^2.2.0", "itertools": "^2.5.0", @@ -78,28 +84,28 @@ "lodash-es": "^4.17.21", "merge-refs": "^2.0.0", "millify": "^6.1.0", - "motion": "^12.23.22", + "motion": "^12.23.24", "numbro": "^2.5.0", "qrcode": "^1.5.4", "qs": "^6.14.0", "radash": "^12.1.1", - "react": "^19.1.1", + "react": "^19.2.0", "react-click-away-listener": "^2.4.0", - "react-datepicker": "^8.7.0", - "react-dom": "^19.1.1", - "react-hook-form": "^7.63.0", + "react-datepicker": "^8.9.0", + "react-dom": "^19.2.0", + "react-hook-form": "^7.66.0", "react-idle-timer": "^5.7.2", "react-intersection-observer": "^9.16.0", - "react-is": "^19.1.1", + "react-is": "^19.2.0", "react-loading-skeleton": "^3.5.0", "react-markdown": "^10.1.0", "react-qr-code": "^2.0.18", "react-resize-detector": "^12.3.0", - "react-router": "^6.30.1", - "react-router-dom": "^6.30.1", + "react-router": "^6.30.2", + "react-router-dom": "^6.30.2", "react-tracked": "^2.0.1", "react-virtualized-auto-sizer": "^1.0.26", - "recharts": "^3.2.1", + "recharts": "^3.4.1", "rehype-external-links": "^3.0.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", @@ -107,12 +113,12 @@ "scheduler": "^0.26.0", "text-case": "^1.2.9", "typesafe-i18n": "^5.26.2", - "use-breakpoint": "^4.0.6", + "use-breakpoint": "^4.0.10", "zod": "^3.25.76", "zustand": "^5.0.8" }, "devDependencies": { - "@babel/core": "^7.28.4", + "@babel/core": "^7.28.5", "@biomejs/biome": "2.2.2", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -122,24 +128,26 @@ "@types/file-saver": "^2.0.7", "@types/humanize-duration": "^3.27.4", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.5.2", + "@types/node": "^24.10.1", "@types/qs": "^6.14.0", - "@types/react": "^19.1.13", - "@types/react-dom": "^19.1.9", + "@types/react": "^19.2.4", + "@types/react-dom": "^19.2.3", "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react-swc": "^4.1.0", - "autoprefixer": "^10.4.21", + "@vitejs/plugin-react-swc": "^4.2.2", + "@vitest/ui": "^4.0.14", + "autoprefixer": "^10.4.22", "concurrently": "^9.2.1", - "dotenv": "^17.2.2", - "esbuild": "^0.25.10", - "globals": "^16.4.0", + "dotenv": "^17.2.3", + "esbuild": "^0.25.12", + "globals": "^16.5.0", "postcss": "^8.5.6", "prettier": "^3.6.2", "sass": "~1.70.0", "standard-version": "^9.5.0", "type-fest": "^4.41.0", - "typescript": "~5.9.2", - "vite": "^7.1.7", - "vite-plugin-package-version": "^1.1.0" + "typescript": "~5.9.3", + "vite": "^7.2.2", + "vite-plugin-package-version": "^1.1.0", + "vitest": "^4.0.14" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index a8738b8914..5c11d27296 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -4,25 +4,28 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + mdast-util-to-hast: 13.2.1 + importers: .: dependencies: '@floating-ui/react': specifier: ^0.27.16 - version: 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@github/webauthn-json': specifier: ^2.1.1 version: 2.1.1 '@hookform/resolvers': specifier: ^5.2.2 - version: 5.2.2(react-hook-form@7.63.0(react@19.1.1)) + version: 5.2.2(react-hook-form@7.66.0(react@19.2.0)) '@react-hook/resize-observer': specifier: ^2.0.2 - version: 2.0.2(react@19.1.1) + version: 2.0.2(react@19.2.0) '@react-rxjs/core': specifier: ^0.10.8 - version: 0.10.8(react@19.1.1)(rxjs@7.8.2) + version: 0.10.8(react@19.2.0)(rxjs@7.8.2) '@stablelib/base64': specifier: ^2.0.1 version: 2.0.1 @@ -30,23 +33,23 @@ importers: specifier: ^2.0.1 version: 2.0.1 '@tanstack/query-core': - specifier: ^5.90.2 - version: 5.90.2 + specifier: ^5.90.9 + version: 5.90.9 '@tanstack/react-query': - specifier: ^5.90.2 - version: 5.90.2(react@19.1.1) + specifier: ^5.90.9 + version: 5.90.9(react@19.2.0) '@tanstack/react-virtual': specifier: 3.13.12 - version: 3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/virtual-core': specifier: 3.13.12 version: 3.13.12 '@use-gesture/react': specifier: ^10.3.1 - version: 10.3.1(react@19.1.1) + version: 10.3.1(react@19.2.0) axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.2 + version: 1.13.2 byte-size: specifier: ^9.0.1 version: 9.0.1 @@ -60,8 +63,8 @@ importers: specifier: ^4.1.0 version: 4.1.0 dayjs: - specifier: ^1.11.18 - version: 1.11.18 + specifier: ^1.11.19 + version: 1.11.19 deepmerge-ts: specifier: ^7.1.5 version: 7.1.5 @@ -90,8 +93,8 @@ importers: specifier: ^5.0.0 version: 5.0.0 html-react-parser: - specifier: ^5.2.6 - version: 5.2.6(@types/react@19.1.13)(react@19.1.1) + specifier: ^5.2.8 + version: 5.2.8(@types/react@19.2.4)(react@19.2.0) humanize-duration: specifier: ^3.33.1 version: 3.33.1 @@ -109,13 +112,13 @@ importers: version: 4.17.21 merge-refs: specifier: ^2.0.0 - version: 2.0.0(@types/react@19.1.13) + version: 2.0.0(@types/react@19.2.4) millify: specifier: ^6.1.0 version: 6.1.0 motion: - specifier: ^12.23.22 - version: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^12.23.24 + version: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) numbro: specifier: ^2.5.0 version: 2.5.0 @@ -129,56 +132,56 @@ importers: specifier: ^12.1.1 version: 12.1.1 react: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.0 + version: 19.2.0 react-click-away-listener: specifier: ^2.4.0 - version: 2.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 2.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-datepicker: - specifier: ^8.7.0 - version: 8.7.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^8.9.0 + version: 8.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-dom: - specifier: ^19.1.1 - version: 19.1.1(react@19.1.1) + specifier: ^19.2.0 + version: 19.2.0(react@19.2.0) react-hook-form: - specifier: ^7.63.0 - version: 7.63.0(react@19.1.1) + specifier: ^7.66.0 + version: 7.66.0(react@19.2.0) react-idle-timer: specifier: ^5.7.2 - version: 5.7.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 5.7.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-intersection-observer: specifier: ^9.16.0 - version: 9.16.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 9.16.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-is: - specifier: ^19.1.1 - version: 19.1.1 + specifier: ^19.2.0 + version: 19.2.0 react-loading-skeleton: specifier: ^3.5.0 - version: 3.5.0(react@19.1.1) + version: 3.5.0(react@19.2.0) react-markdown: specifier: ^10.1.0 - version: 10.1.0(@types/react@19.1.13)(react@19.1.1) + version: 10.1.0(@types/react@19.2.4)(react@19.2.0) react-qr-code: specifier: ^2.0.18 - version: 2.0.18(react@19.1.1) + version: 2.0.18(react@19.2.0) react-resize-detector: specifier: ^12.3.0 - version: 12.3.0(react@19.1.1) + version: 12.3.0(react@19.2.0) react-router: - specifier: ^6.30.1 - version: 6.30.1(react@19.1.1) + specifier: ^6.30.2 + version: 6.30.2(react@19.2.0) react-router-dom: - specifier: ^6.30.1 - version: 6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^6.30.2 + version: 6.30.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-tracked: specifier: ^2.0.1 - version: 2.0.1(react@19.1.1)(scheduler@0.26.0) + version: 2.0.1(react@19.2.0)(scheduler@0.26.0) react-virtualized-auto-sizer: specifier: ^1.0.26 - version: 1.0.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 1.0.26(react-dom@19.2.0(react@19.2.0))(react@19.2.0) recharts: - specifier: ^3.2.1 - version: 3.2.1(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1) + specifier: ^3.4.1 + version: 3.4.1(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1) rehype-external-links: specifier: ^3.0.0 version: 3.0.0 @@ -199,20 +202,20 @@ importers: version: 1.2.9 typesafe-i18n: specifier: ^5.26.2 - version: 5.26.2(typescript@5.9.2) + version: 5.26.2(typescript@5.9.3) use-breakpoint: - specifier: ^4.0.6 - version: 4.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + specifier: ^4.0.10 + version: 4.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0) zod: specifier: ^3.25.76 version: 3.25.76 zustand: specifier: ^5.0.8 - version: 5.0.8(@types/react@19.1.13)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)) + version: 5.0.8(@types/react@19.2.4)(immer@10.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)) devDependencies: '@babel/core': - specifier: ^7.28.4 - version: 7.28.4 + specifier: ^7.28.5 + version: 7.28.5 '@biomejs/biome': specifier: 2.2.2 version: 2.2.2 @@ -224,10 +227,10 @@ importers: version: 3.0.4 '@hookform/devtools': specifier: ^4.4.0 - version: 4.4.0(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + version: 4.4.0(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@tanstack/react-query-devtools': specifier: ^5.90.2 - version: 5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1) + version: 5.90.2(@tanstack/react-query@5.90.9(react@19.2.0))(react@19.2.0) '@types/byte-size': specifier: ^8.1.2 version: 8.1.2 @@ -241,38 +244,41 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.5.2 - version: 24.5.2 + specifier: ^24.10.1 + version: 24.10.1 '@types/qs': specifier: ^6.14.0 version: 6.14.0 '@types/react': - specifier: ^19.1.13 - version: 19.1.13 + specifier: ^19.2.4 + version: 19.2.4 '@types/react-dom': - specifier: ^19.1.9 - version: 19.1.9(@types/react@19.1.13) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.4) '@types/react-router-dom': specifier: ^5.3.3 version: 5.3.3 '@vitejs/plugin-react-swc': - specifier: ^4.1.0 - version: 4.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + specifier: ^4.2.2 + version: 4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + '@vitest/ui': + specifier: ^4.0.14 + version: 4.0.14(vitest@4.0.14) autoprefixer: - specifier: ^10.4.21 - version: 10.4.21(postcss@8.5.6) + specifier: ^10.4.22 + version: 10.4.22(postcss@8.5.6) concurrently: specifier: ^9.2.1 version: 9.2.1 dotenv: - specifier: ^17.2.2 - version: 17.2.2 + specifier: ^17.2.3 + version: 17.2.3 esbuild: - specifier: ^0.25.10 - version: 0.25.10 + specifier: ^0.25.12 + version: 0.25.12 globals: - specifier: ^16.4.0 - version: 16.4.0 + specifier: ^16.5.0 + version: 16.5.0 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -289,14 +295,17 @@ importers: specifier: ^4.41.0 version: 4.41.0 typescript: - specifier: ~5.9.2 - version: 5.9.2 + specifier: ~5.9.3 + version: 5.9.3 vite: - specifier: ^7.1.7 - version: 7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + specifier: ^7.2.2 + version: 7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) vite-plugin-package-version: specifier: ^1.1.0 - version: 1.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + version: 1.1.0(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + vitest: + specifier: ^4.0.14 + version: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) packages: @@ -304,16 +313,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': @@ -338,8 +347,8 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -350,8 +359,8 @@ packages: resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -363,12 +372,12 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} '@biomejs/biome@2.2.2': @@ -488,158 +497,158 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@esbuild/aix-ppc64@0.25.10': - resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.10': - resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.10': - resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.10': - resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.10': - resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.10': - resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.10': - resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.10': - resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.10': - resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.10': - resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.10': - resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.10': - resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.10': - resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.10': - resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.10': - resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.10': - resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.10': - resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.10': - resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.10': - resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.10': - resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.10': - resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.10': - resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.10': - resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.10': - resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.10': - resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.10': - resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -704,6 +713,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@react-hook/latest@1.0.3': resolution: {integrity: sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg==} peerDependencies: @@ -725,8 +737,8 @@ packages: react: '>=16.8.0' rxjs: '>=7' - '@reduxjs/toolkit@2.9.0': - resolution: {integrity: sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==} + '@reduxjs/toolkit@2.10.1': + resolution: {integrity: sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==} peerDependencies: react: ^16.9.0 || ^17.0.0 || ^18 || ^19 react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 @@ -736,120 +748,120 @@ packages: react-redux: optional: true - '@remix-run/router@1.23.0': - resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} + '@remix-run/router@1.23.1': + resolution: {integrity: sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==} engines: {node: '>=14.0.0'} - '@rolldown/pluginutils@1.0.0-beta.35': - resolution: {integrity: sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==} + '@rolldown/pluginutils@1.0.0-beta.47': + resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rollup/rollup-android-arm-eabi@4.52.2': - resolution: {integrity: sha512-o3pcKzJgSGt4d74lSZ+OCnHwkKBeAbFDmbEm5gg70eA8VkyCuC/zV9TwBnmw6VjDlRdF4Pshfb+WE9E6XY1PoQ==} + '@rollup/rollup-android-arm-eabi@4.53.2': + resolution: {integrity: sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.52.2': - resolution: {integrity: sha512-cqFSWO5tX2vhC9hJTK8WAiPIm4Q8q/cU8j2HQA0L3E1uXvBYbOZMhE2oFL8n2pKB5sOCHY6bBuHaRwG7TkfJyw==} + '@rollup/rollup-android-arm64@4.53.2': + resolution: {integrity: sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.52.2': - resolution: {integrity: sha512-vngduywkkv8Fkh3wIZf5nFPXzWsNsVu1kvtLETWxTFf/5opZmflgVSeLgdHR56RQh71xhPhWoOkEBvbehwTlVA==} + '@rollup/rollup-darwin-arm64@4.53.2': + resolution: {integrity: sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.52.2': - resolution: {integrity: sha512-h11KikYrUCYTrDj6h939hhMNlqU2fo/X4NB0OZcys3fya49o1hmFaczAiJWVAFgrM1NCP6RrO7lQKeVYSKBPSQ==} + '@rollup/rollup-darwin-x64@4.53.2': + resolution: {integrity: sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.52.2': - resolution: {integrity: sha512-/eg4CI61ZUkLXxMHyVlmlGrSQZ34xqWlZNW43IAU4RmdzWEx0mQJ2mN/Cx4IHLVZFL6UBGAh+/GXhgvGb+nVxw==} + '@rollup/rollup-freebsd-arm64@4.53.2': + resolution: {integrity: sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.52.2': - resolution: {integrity: sha512-QOWgFH5X9+p+S1NAfOqc0z8qEpJIoUHf7OWjNUGOeW18Mx22lAUOiA9b6r2/vpzLdfxi/f+VWsYjUOMCcYh0Ng==} + '@rollup/rollup-freebsd-x64@4.53.2': + resolution: {integrity: sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': - resolution: {integrity: sha512-kDWSPafToDd8LcBYd1t5jw7bD5Ojcu12S3uT372e5HKPzQt532vW+rGFFOaiR0opxePyUkHrwz8iWYEyH1IIQA==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.52.2': - resolution: {integrity: sha512-gKm7Mk9wCv6/rkzwCiUC4KnevYhlf8ztBrDRT9g/u//1fZLapSRc+eDZj2Eu2wpJ+0RzUKgtNijnVIB4ZxyL+w==} + '@rollup/rollup-linux-arm-musleabihf@4.53.2': + resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.52.2': - resolution: {integrity: sha512-66lA8vnj5mB/rtDNwPgrrKUOtCLVQypkyDa2gMfOefXK6rcZAxKLO9Fy3GkW8VkPnENv9hBkNOFfGLf6rNKGUg==} + '@rollup/rollup-linux-arm64-gnu@4.53.2': + resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.52.2': - resolution: {integrity: sha512-s+OPucLNdJHvuZHuIz2WwncJ+SfWHFEmlC5nKMUgAelUeBUnlB4wt7rXWiyG4Zn07uY2Dd+SGyVa9oyLkVGOjA==} + '@rollup/rollup-linux-arm64-musl@4.53.2': + resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.52.2': - resolution: {integrity: sha512-8wTRM3+gVMDLLDdaT6tKmOE3lJyRy9NpJUS/ZRWmLCmOPIJhVyXwjBo+XbrrwtV33Em1/eCTd5TuGJm4+DmYjw==} + '@rollup/rollup-linux-loong64-gnu@4.53.2': + resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.52.2': - resolution: {integrity: sha512-6yqEfgJ1anIeuP2P/zhtfBlDpXUb80t8DpbYwXQ3bQd95JMvUaqiX+fKqYqUwZXqdJDd8xdilNtsHM2N0cFm6A==} + '@rollup/rollup-linux-ppc64-gnu@4.53.2': + resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.52.2': - resolution: {integrity: sha512-sshYUiYVSEI2B6dp4jMncwxbrUqRdNApF2c3bhtLAU0qA8Lrri0p0NauOsTWh3yCCCDyBOjESHMExonp7Nzc0w==} + '@rollup/rollup-linux-riscv64-gnu@4.53.2': + resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.52.2': - resolution: {integrity: sha512-duBLgd+3pqC4MMwBrKkFxaZerUxZcYApQVC5SdbF5/e/589GwVvlRUnyqMFbM8iUSb1BaoX/3fRL7hB9m2Pj8Q==} + '@rollup/rollup-linux-riscv64-musl@4.53.2': + resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.52.2': - resolution: {integrity: sha512-tzhYJJidDUVGMgVyE+PmxENPHlvvqm1KILjjZhB8/xHYqAGeizh3GBGf9u6WdJpZrz1aCpIIHG0LgJgH9rVjHQ==} + '@rollup/rollup-linux-s390x-gnu@4.53.2': + resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.52.2': - resolution: {integrity: sha512-opH8GSUuVcCSSyHHcl5hELrmnk4waZoVpgn/4FDao9iyE4WpQhyWJ5ryl5M3ocp4qkRuHfyXnGqg8M9oKCEKRA==} + '@rollup/rollup-linux-x64-gnu@4.53.2': + resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.52.2': - resolution: {integrity: sha512-LSeBHnGli1pPKVJ79ZVJgeZWWZXkEe/5o8kcn23M8eMKCUANejchJbF/JqzM4RRjOJfNRhKJk8FuqL1GKjF5oQ==} + '@rollup/rollup-linux-x64-musl@4.53.2': + resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==} cpu: [x64] os: [linux] - '@rollup/rollup-openharmony-arm64@4.52.2': - resolution: {integrity: sha512-uPj7MQ6/s+/GOpolavm6BPo+6CbhbKYyZHUDvZ/SmJM7pfDBgdGisFX3bY/CBDMg2ZO4utfhlApkSfZ92yXw7Q==} + '@rollup/rollup-openharmony-arm64@4.53.2': + resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.52.2': - resolution: {integrity: sha512-Z9MUCrSgIaUeeHAiNkm3cQyst2UhzjPraR3gYYfOjAuZI7tcFRTOD+4cHLPoS/3qinchth+V56vtqz1Tv+6KPA==} + '@rollup/rollup-win32-arm64-msvc@4.53.2': + resolution: {integrity: sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.52.2': - resolution: {integrity: sha512-+GnYBmpjldD3XQd+HMejo+0gJGwYIOfFeoBQv32xF/RUIvccUz20/V6Otdv+57NE70D5pa8W/jVGDoGq0oON4A==} + '@rollup/rollup-win32-ia32-msvc@4.53.2': + resolution: {integrity: sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.52.2': - resolution: {integrity: sha512-ApXFKluSB6kDQkAqZOKXBjiaqdF1BlKi+/eqnYe9Ee7U2K3pUDKsIyr8EYm/QDHTJIM+4X+lI0gJc3TTRhd+dA==} + '@rollup/rollup-win32-x64-gnu@4.53.2': + resolution: {integrity: sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.52.2': - resolution: {integrity: sha512-ARz+Bs8kY6FtitYM96PqPEVvPXqEZmPZsSkXvyX19YzDqkCaIlhCieLLMI5hxO9SRZ2XtCtm8wxhy0iJ2jxNfw==} + '@rollup/rollup-win32-x64-msvc@4.53.2': + resolution: {integrity: sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==} cpu: [x64] os: [win32] @@ -888,68 +900,68 @@ packages: '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} - '@swc/core-darwin-arm64@1.13.19': - resolution: {integrity: sha512-NxDyte9tCJSJ8+R62WDtqwg8eI57lubD52sHyGOfezpJBOPr36bUSGGLyO3Vod9zTGlOu2CpkuzA/2iVw92u1g==} + '@swc/core-darwin-arm64@1.15.2': + resolution: {integrity: sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==} engines: {node: '>=10'} cpu: [arm64] os: [darwin] - '@swc/core-darwin-x64@1.13.19': - resolution: {integrity: sha512-+w5DYrJndSygFFRDcuPYmx5BljD6oYnAohZ15K1L6SfORHp/BTSIbgSFRKPoyhjuIkDiq3W0um8RoMTOBAcQjQ==} + '@swc/core-darwin-x64@1.15.2': + resolution: {integrity: sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==} engines: {node: '>=10'} cpu: [x64] os: [darwin] - '@swc/core-linux-arm-gnueabihf@1.13.19': - resolution: {integrity: sha512-7LlfgpdwwYq2q7himNkAAFo4q6jysMLFNoBH6GRP7WL29NcSsl5mPMJjmYZymK+sYq/9MTVieDTQvChzYDsapw==} + '@swc/core-linux-arm-gnueabihf@1.15.2': + resolution: {integrity: sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==} engines: {node: '>=10'} cpu: [arm] os: [linux] - '@swc/core-linux-arm64-gnu@1.13.19': - resolution: {integrity: sha512-ml3I6Lm2marAQ3UC/TS9t/yILBh/eDSVHAdPpikp652xouWAVW1znUeV6bBSxe1sSZIenv+p55ubKAWq/u84sQ==} + '@swc/core-linux-arm64-gnu@1.15.2': + resolution: {integrity: sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-arm64-musl@1.13.19': - resolution: {integrity: sha512-M/otFc3/rWWkbF6VgbOXVzUKVoE7MFcphTaStxJp4bwb7oP5slYlxMZN51Dk/OTOfvCDo9pTAFDKNyixbkXMDQ==} + '@swc/core-linux-arm64-musl@1.15.2': + resolution: {integrity: sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==} engines: {node: '>=10'} cpu: [arm64] os: [linux] - '@swc/core-linux-x64-gnu@1.13.19': - resolution: {integrity: sha512-NoMUKaOJEdouU4tKF88ggdDHFiRRING+gYLxDqnTfm+sUXaizB5OGBRzvSVDYSXQb1SuUuChnXFPFzwTWbt3ZQ==} + '@swc/core-linux-x64-gnu@1.15.2': + resolution: {integrity: sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-linux-x64-musl@1.13.19': - resolution: {integrity: sha512-r6krlZwyu8SBaw24QuS1lau2I9q8M+eJV6ITz0rpb6P1Bx0elf9ii5Bhh8ddmIqXXH8kOGSjC/dwcdHbZqAhgw==} + '@swc/core-linux-x64-musl@1.15.2': + resolution: {integrity: sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==} engines: {node: '>=10'} cpu: [x64] os: [linux] - '@swc/core-win32-arm64-msvc@1.13.19': - resolution: {integrity: sha512-awcZSIuxyVn0Dw28VjMvgk1qiDJ6CeQwHkZNUjg2UxVlq23zE01NMMp+zkoGFypmLG9gaGmJSzuoqvk/WCQ5tw==} + '@swc/core-win32-arm64-msvc@1.15.2': + resolution: {integrity: sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==} engines: {node: '>=10'} cpu: [arm64] os: [win32] - '@swc/core-win32-ia32-msvc@1.13.19': - resolution: {integrity: sha512-H5d+KO7ISoLNgYvTbOcCQjJZNM3R7yaYlrMAF13lUr6GSiOUX+92xtM31B+HvzAWI7HtvVe74d29aC1b1TpXFA==} + '@swc/core-win32-ia32-msvc@1.15.2': + resolution: {integrity: sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==} engines: {node: '>=10'} cpu: [ia32] os: [win32] - '@swc/core-win32-x64-msvc@1.13.19': - resolution: {integrity: sha512-qNoyCpXvv2O3JqXKanRIeoMn03Fho/As+N4Fhe7u0FsYh4VYqGQah4DGDzEP/yjl4Gx1IElhqLGDhCCGMwWaDw==} + '@swc/core-win32-x64-msvc@1.15.2': + resolution: {integrity: sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==} engines: {node: '>=10'} cpu: [x64] os: [win32] - '@swc/core@1.13.19': - resolution: {integrity: sha512-V1r4wFdjaZIUIZZrV2Mb/prEeu03xvSm6oatPxsvnXKF9lNh5Jtk9QvUdiVfD9rrvi7bXrAVhg9Wpbmv/2Fl1g==} + '@swc/core@1.15.2': + resolution: {integrity: sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==} engines: {node: '>=10'} peerDependencies: '@swc/helpers': '>=0.5.17' @@ -963,8 +975,8 @@ packages: '@swc/types@0.1.25': resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==} - '@tanstack/query-core@5.90.2': - resolution: {integrity: sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==} + '@tanstack/query-core@5.90.9': + resolution: {integrity: sha512-UFOCQzi6pRGeVTVlPNwNdnAvT35zugcIydqjvFUzG62dvz2iVjElmNp/hJkUoM5eqbUPfSU/GJIr/wbvD8bTUw==} '@tanstack/query-devtools@5.90.1': resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==} @@ -975,8 +987,8 @@ packages: '@tanstack/react-query': ^5.90.2 react: ^18 || ^19 - '@tanstack/react-query@5.90.2': - resolution: {integrity: sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==} + '@tanstack/react-query@5.90.9': + resolution: {integrity: sha512-Zke2AaXiaSfnG8jqPZR52m8SsclKT2d9//AgE/QIzyNvbpj/Q2ln+FsZjb1j69bJZUouBvX2tg9PHirkTm8arw==} peerDependencies: react: ^18 || ^19 @@ -992,6 +1004,9 @@ packages: '@types/byte-size@8.1.2': resolution: {integrity: sha512-jGyVzYu6avI8yuqQCNTZd65tzI8HZrLjKX9sdMqZrGWVlNChu0rf6p368oVEDCYJe5BMx2Ov04tD1wqtgTwGSA==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} @@ -1022,6 +1037,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree-jsx@1.0.5': resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} @@ -1055,8 +1073,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.5.2': - resolution: {integrity: sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1067,10 +1085,10 @@ packages: '@types/qs@6.14.0': resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} - '@types/react-dom@19.1.9': - resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^19.0.0 + '@types/react': ^19.2.0 '@types/react-router-dom@5.3.3': resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} @@ -1078,8 +1096,8 @@ packages: '@types/react-router@5.1.20': resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react@19.1.13': - resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + '@types/react@19.2.4': + resolution: {integrity: sha512-tBFxBp9Nfyy5rsmefN+WXc1JeW/j2BpBHFdLZbEVfs9wn3E3NRFxwV0pJg8M1qQAexFpvz73hJXFofV0ZAu92A==} '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1101,12 +1119,46 @@ packages: peerDependencies: react: '>= 16.8.0' - '@vitejs/plugin-react-swc@4.1.0': - resolution: {integrity: sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==} + '@vitejs/plugin-react-swc@4.2.2': + resolution: {integrity: sha512-x+rE6tsxq/gxrEJN3Nv3dIV60lFflPj94c90b+NNo6n1QV1QQUTLoL0MpaOVasUZ0zqVBn7ead1B5ecx1JAGfA==} engines: {node: ^20.19.0 || >=22.12.0} peerDependencies: vite: ^4 || ^5 || ^6 || ^7 + '@vitest/expect@4.0.14': + resolution: {integrity: sha512-RHk63V3zvRiYOWAV0rGEBRO820ce17hz7cI2kDmEdfQsBjT2luEKB5tCOc91u1oSQoUOZkSv3ZyzkdkSLD7lKw==} + + '@vitest/mocker@4.0.14': + resolution: {integrity: sha512-RzS5NujlCzeRPF1MK7MXLiEFpkIXeMdQ+rN3Kk3tDI9j0mtbr7Nmuq67tpkOJQpgyClbOltCXMjLZicJHsH5Cg==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.14': + resolution: {integrity: sha512-SOYPgujB6TITcJxgd3wmsLl+wZv+fy3av2PpiPpsWPZ6J1ySUYfScfpIt2Yv56ShJXR2MOA6q2KjKHN4EpdyRQ==} + + '@vitest/runner@4.0.14': + resolution: {integrity: sha512-BsAIk3FAqxICqREbX8SetIteT8PiaUL/tgJjmhxJhCsigmzzH8xeadtp7LRnTpCVzvf0ib9BgAfKJHuhNllKLw==} + + '@vitest/snapshot@4.0.14': + resolution: {integrity: sha512-aQVBfT1PMzDSA16Y3Fp45a0q8nKexx6N5Amw3MX55BeTeZpoC08fGqEZqVmPcqN0ueZsuUQ9rriPMhZ3Mu19Ag==} + + '@vitest/spy@4.0.14': + resolution: {integrity: sha512-JmAZT1UtZooO0tpY3GRyiC/8W7dCs05UOq9rfsUUgEZEdq+DuHLmWhPsrTt0TiW7WYeL/hXpaE07AZ2RCk44hg==} + + '@vitest/ui@4.0.14': + resolution: {integrity: sha512-fvDz8o7SQpFLoSBo6Cudv+fE85/fPCkwTnLAN85M+Jv7k59w2mSIjT9Q5px7XwGrmYqqKBEYxh/09IBGd1E7AQ==} + peerDependencies: + vitest: 4.0.14 + + '@vitest/utils@4.0.14': + resolution: {integrity: sha512-hLqXZKAWNg8pI+SQXyXxWCTOpA3MvsqcbVeNgSi8x/CSN2wi26dSzn1wrOhmCmFjEvN9p8/kLFRHa6PI8jHazw==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -1142,18 +1194,22 @@ packages: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - autoprefixer@10.4.21: - resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} + autoprefixer@10.4.22: + resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - axios@1.12.2: - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} babel-plugin-macros@3.1.0: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} @@ -1165,8 +1221,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.7: - resolution: {integrity: sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==} + baseline-browser-mapping@2.8.28: + resolution: {integrity: sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==} hasBin: true bignumber.js@9.3.1: @@ -1183,8 +1239,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.2: - resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==} + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1220,12 +1276,16 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001745: - resolution: {integrity: sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.1: + resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -1388,8 +1448,8 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csstype@3.2.0: + resolution: {integrity: sha512-si++xzRAY9iPp60roQiFta7OFbhrgvcthrhlNAGeQptSY25uJjkfUV8OArC3KLocB8JT8ohz+qgxWCmz8RhjIg==} d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} @@ -1445,8 +1505,8 @@ packages: dateformat@3.0.3: resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} - dayjs@1.11.18: - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -1521,8 +1581,8 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} - dotenv@17.2.2: - resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} dotgitignore@2.1.0: @@ -1533,8 +1593,8 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - electron-to-chromium@1.5.224: - resolution: {integrity: sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==} + electron-to-chromium@1.5.252: + resolution: {integrity: sha512-53uTpjtRgS7gjIxZ4qCgFdNO2q+wJt/Z8+xAvxbCqXPJrY6h7ighUkadQmNMXH96crtpa6gPFNP7BF4UBGDuaA==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -1558,6 +1618,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -1566,11 +1629,11 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-toolkit@1.39.10: - resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + es-toolkit@1.41.0: + resolution: {integrity: sha512-bDd3oRmbVgqZCJS6WmeQieOrzpl3URcWBUVDXxOELlUW2FuW+0glPOz1n0KnRie+PdyvUZcXz2sOn00c6pPRIA==} - esbuild@0.25.10: - resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -1589,6 +1652,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} @@ -1596,6 +1662,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -1611,6 +1681,9 @@ packages: picomatch: optional: true + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -1641,6 +1714,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} @@ -1654,11 +1730,11 @@ packages: resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - framer-motion@12.23.22: - resolution: {integrity: sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==} + framer-motion@12.23.24: + resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -1728,8 +1804,8 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - globals@16.4.0: - resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + globals@16.5.0: + resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} gopd@1.2.0: @@ -1812,8 +1888,8 @@ packages: html-dom-parser@5.1.1: resolution: {integrity: sha512-+o4Y4Z0CLuyemeccvGN4bAO20aauB2N9tFEAep5x4OW34kV4PTarBHm6RL02afYt2BMKcr0D2Agep8S3nJPIBg==} - html-react-parser@5.2.6: - resolution: {integrity: sha512-qcpPWLaSvqXi+TndiHbCa+z8qt0tVzjMwFGFBAa41ggC+ZA5BHaMIeMJla9g3VSp4SmiZb9qyQbmbpHYpIfPOg==} + html-react-parser@5.2.8: + resolution: {integrity: sha512-09WaI81tbpwhXWeMe1m9VptZVJUcigo0l59zVt+2HUIQT7+baU38/oNhllj6MKhOuGXqh0nrlwOgxbxbm6xXHw==} peerDependencies: '@types/react': 0.14 || 15 || 16 || 17 || 18 || 19 react: 0.14 || 15 || 16 || 17 || 18 || 19 @@ -1833,8 +1909,8 @@ packages: humanize-duration@3.33.1: resolution: {integrity: sha512-hwzSCymnRdFx9YdRkQQ0OYequXiVAV6ZGQA2uzocwB0F4309Ke6pO8dg0P8LHhRQJyVjGteRTAA/zNfEcpXn8A==} - immer@10.1.3: - resolution: {integrity: sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} immutable@4.3.7: resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} @@ -1853,8 +1929,8 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.6: + resolution: {integrity: sha512-gtGXVaBdl5mAes3rPcMedEBm12ibjt1kDMFfheul1wUAOVEJW60voNdMVzVkfLN06O7ZaD/rxhfKgtlgtTbMjg==} internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} @@ -2017,6 +2093,9 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2044,8 +2123,8 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} - mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -2158,14 +2237,14 @@ packages: resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} engines: {node: '>=0.10.0'} - motion-dom@12.23.21: - resolution: {integrity: sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==} + motion-dom@12.23.23: + resolution: {integrity: sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==} motion-utils@12.23.6: resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} - motion@12.23.22: - resolution: {integrity: sha512-iSq6X9vLHbeYwmHvhK//+U74ROaPnZmBuy60XZzqNl0QtZkWfoZyMDHYnpKuWFv0sNMqHgED8aCXk94LCoQPGg==} + motion@12.23.24: + resolution: {integrity: sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2178,6 +2257,10 @@ packages: react-dom: optional: true + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2192,8 +2275,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.21: - resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -2221,6 +2304,9 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + p-limit@1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -2294,6 +2380,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2381,19 +2470,19 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-datepicker@8.7.0: - resolution: {integrity: sha512-r5OJbiLWc3YiVNy69Kau07/aVgVGsFVMA6+nlqCV7vyQ8q0FUOnJ+wAI4CgVxHejG3i5djAEiebrF8/Eip4rIw==} + react-datepicker@8.9.0: + resolution: {integrity: sha512-yoRsGxjqVRjk8iUBssrW9jcinTeyP9mAfTpuzdKvlESOUjdrY0sfDTzIZWJAn38jvNcxW1dnDmW1CinjiFdxYQ==} peerDependencies: react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom@19.1.1: - resolution: {integrity: sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==} + react-dom@19.2.0: + resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: - react: ^19.1.1 + react: ^19.2.0 - react-hook-form@7.63.0: - resolution: {integrity: sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==} + react-hook-form@7.66.0: + resolution: {integrity: sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -2416,8 +2505,8 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - react-is@19.1.1: - resolution: {integrity: sha512-tr41fA15Vn8p4X9ntI+yCyeGSf1TlYaY5vlTZfQmeLBrFo3psOPX6HhTDnFNL9uj3EhP0KAQ80cugCl4b4BERA==} + react-is@19.2.0: + resolution: {integrity: sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==} react-loading-skeleton@3.5.0: resolution: {integrity: sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==} @@ -2455,15 +2544,15 @@ packages: peerDependencies: react: ^18.0.0 || ^19.0.0 - react-router-dom@6.30.1: - resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} + react-router-dom@6.30.2: + resolution: {integrity: sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' react-dom: '>=16.8' - react-router@6.30.1: - resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} + react-router@6.30.2: + resolution: {integrity: sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==} engines: {node: '>=14.0.0'} peerDependencies: react: '>=16.8' @@ -2485,8 +2574,8 @@ packages: react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.1.1: - resolution: {integrity: sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==} + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} read-pkg-up@3.0.0: @@ -2516,8 +2605,8 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} - recharts@3.2.1: - resolution: {integrity: sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw==} + recharts@3.4.1: + resolution: {integrity: sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==} engines: {node: '>=18'} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2565,13 +2654,13 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} hasBin: true - rollup@4.52.2: - resolution: {integrity: sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==} + rollup@4.53.2: + resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2592,6 +2681,9 @@ packages: scheduler@0.26.0: resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -2600,8 +2692,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true @@ -2628,6 +2720,13 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2664,11 +2763,17 @@ packages: split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-version@9.5.0: resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} engines: {node: '>=10'} hasBin: true + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2698,11 +2803,11 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} - style-to-js@1.1.17: - resolution: {integrity: sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==} + style-to-js@1.1.19: + resolution: {integrity: sha512-Ev+SgeqiNGT1ufsXyVC5RrJRXdrkRJ1Gol9Qw7Pb72YCKJXrBvP0ckZhBeVSrw2m06DJpei2528uIpjMb4TsoQ==} - style-to-object@1.0.9: - resolution: {integrity: sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==} + style-to-object@1.0.12: + resolution: {integrity: sha512-ddJqYnoT4t97QvN2C95bCgt+m7AAgXjVnkk/jxAfmp7EAB8nnqqZYEbMd3em7/vEomDb2LAQKAy1RFfv41mdNw==} stylis@4.2.0: resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} @@ -2723,8 +2828,8 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - tabbable@6.2.0: - resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + tabbable@6.3.0: + resolution: {integrity: sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==} terser@5.37.0: resolution: {integrity: sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==} @@ -2810,14 +2915,28 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -2860,8 +2979,8 @@ packages: peerDependencies: typescript: '>=3.5.1' - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true @@ -2870,14 +2989,14 @@ packages: engines: {node: '>=0.8.0'} hasBin: true - undici-types@7.12.0: - resolution: {integrity: sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} unified@11.0.5: resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} - unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} @@ -2885,20 +3004,20 @@ packages: unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' - use-breakpoint@4.0.6: - resolution: {integrity: sha512-1s7vUjf36eeZYTgY1KkmPNXrTbKJVRA9cjBFQdYjK8+pDr0qJgH6/cuX5qQ2zcfkqxN5LieVd/DTVK6ofnwRTQ==} + use-breakpoint@4.0.10: + resolution: {integrity: sha512-rnUpZwCQCTtexbpM8S5aiJrfIx6NTvt0WwATiH4hCBN6gQNgkYPFoFt6g/3pAuyqU9D9tLKwXfsVqEWMBnwo6A==} peerDependencies: react: '>=18' react-dom: '>=18' @@ -2915,8 +3034,8 @@ packages: peerDependencies: react: '>=16.13' - use-sync-external-store@1.5.0: - resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2947,8 +3066,8 @@ packages: peerDependencies: vite: '>=2.0.0-beta.69' - vite@7.1.7: - resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + vite@7.2.2: + resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2987,12 +3106,51 @@ packages: yaml: optional: true + vitest@4.0.14: + resolution: {integrity: sha512-d9B2J9Cm9dN9+6nxMnnNJKJCtcyKfnHj15N6YNJfaFHRLua/d3sRKU9RuKmO9mB0XdFtUizlxfz/VPbd3OxGhw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.14 + '@vitest/browser-preview': 4.0.14 + '@vitest/browser-webdriverio': 4.0.14 + '@vitest/ui': 4.0.14 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} @@ -3086,23 +3244,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.4': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.28.4': + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -3112,19 +3270,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.2 + browserslist: 4.28.0 lru-cache: 5.1.1 semver: 6.3.1 @@ -3132,59 +3290,59 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@biomejs/biome@2.2.2': optionalDependencies: @@ -3259,19 +3417,19 @@ snapshots: '@emotion/memoize@0.9.0': {} - '@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1)': + '@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/cache': 11.14.0 '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 '@emotion/weak-memoize': 0.4.0 hoist-non-react-statics: 3.3.2 - react: 19.1.1 + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 transitivePeerDependencies: - supports-color @@ -3281,111 +3439,111 @@ snapshots: '@emotion/memoize': 0.9.0 '@emotion/unitless': 0.10.0 '@emotion/utils': 1.4.2 - csstype: 3.1.3 + csstype: 3.2.0 '@emotion/sheet@1.4.0': {} - '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1)': + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0)': dependencies: '@babel/runtime': 7.28.4 '@emotion/babel-plugin': 11.13.5 '@emotion/is-prop-valid': 1.4.0 - '@emotion/react': 11.14.0(@types/react@19.1.13)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) '@emotion/serialize': 1.3.3 - '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.1.1) + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0) '@emotion/utils': 1.4.2 - react: 19.1.1 + react: 19.2.0 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 transitivePeerDependencies: - supports-color '@emotion/unitless@0.10.0': {} - '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.1.1)': + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.0)': dependencies: - react: 19.1.1 + react: 19.2.0 '@emotion/utils@1.4.2': {} '@emotion/weak-memoize@0.4.0': {} - '@esbuild/aix-ppc64@0.25.10': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.10': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.10': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.10': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.10': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.10': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.10': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.10': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.10': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.10': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.10': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.10': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.10': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.10': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.10': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.10': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.10': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.10': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.10': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.10': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.10': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.10': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.10': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.10': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.10': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.10': + '@esbuild/win32-x64@0.25.12': optional: true '@floating-ui/core@1.7.3': @@ -3397,44 +3555,44 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - '@floating-ui/react@0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@floating-ui/react@0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@floating-ui/utils': 0.2.10 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - tabbable: 6.2.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tabbable: 6.3.0 '@floating-ui/utils@0.2.10': {} '@github/webauthn-json@2.1.1': {} - '@hookform/devtools@4.4.0(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@hookform/devtools@4.4.0(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: - '@emotion/react': 11.14.0(@types/react@19.1.13)(react@19.1.1) - '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.1.13)(react@19.1.1))(@types/react@19.1.13)(react@19.1.1) + '@emotion/react': 11.14.0(@types/react@19.2.4)(react@19.2.0) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.4)(react@19.2.0))(@types/react@19.2.4)(react@19.2.0) '@types/lodash': 4.17.20 - little-state-machine: 4.8.1(react@19.1.1) + little-state-machine: 4.8.1(react@19.2.0) lodash: 4.17.21 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-simple-animate: 3.5.3(react-dom@19.1.1(react@19.1.1)) - use-deep-compare-effect: 1.8.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-simple-animate: 3.5.3(react-dom@19.2.0(react@19.2.0)) + use-deep-compare-effect: 1.8.1(react@19.2.0) uuid: 8.3.2 transitivePeerDependencies: - '@types/react' - supports-color - '@hookform/resolvers@5.2.2(react-hook-form@7.63.0(react@19.1.1))': + '@hookform/resolvers@5.2.2(react-hook-form@7.66.0(react@19.2.0))': dependencies: '@standard-schema/utils': 0.3.0 - react-hook-form: 7.63.0(react@19.1.1) + react-hook-form: 7.66.0(react@19.2.0) '@hutson/parse-repository-url@3.0.2': {} @@ -3463,107 +3621,109 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@react-hook/latest@1.0.3(react@19.1.1)': + '@polka/url@1.0.0-next.29': {} + + '@react-hook/latest@1.0.3(react@19.2.0)': dependencies: - react: 19.1.1 + react: 19.2.0 - '@react-hook/passive-layout-effect@1.2.1(react@19.1.1)': + '@react-hook/passive-layout-effect@1.2.1(react@19.2.0)': dependencies: - react: 19.1.1 + react: 19.2.0 - '@react-hook/resize-observer@2.0.2(react@19.1.1)': + '@react-hook/resize-observer@2.0.2(react@19.2.0)': dependencies: - '@react-hook/latest': 1.0.3(react@19.1.1) - '@react-hook/passive-layout-effect': 1.2.1(react@19.1.1) - react: 19.1.1 + '@react-hook/latest': 1.0.3(react@19.2.0) + '@react-hook/passive-layout-effect': 1.2.1(react@19.2.0) + react: 19.2.0 - '@react-rxjs/core@0.10.8(react@19.1.1)(rxjs@7.8.2)': + '@react-rxjs/core@0.10.8(react@19.2.0)(rxjs@7.8.2)': dependencies: '@rx-state/core': 0.1.4(rxjs@7.8.2) - react: 19.1.1 + react: 19.2.0 rxjs: 7.8.2 - use-sync-external-store: 1.5.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.2.0) - '@reduxjs/toolkit@2.9.0(react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1))(react@19.1.1)': + '@reduxjs/toolkit@2.10.1(react-redux@9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 - immer: 10.1.3 + immer: 10.2.0 redux: 5.0.1 redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 optionalDependencies: - react: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1) + react: 19.2.0 + react-redux: 9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1) - '@remix-run/router@1.23.0': {} + '@remix-run/router@1.23.1': {} - '@rolldown/pluginutils@1.0.0-beta.35': {} + '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rollup/rollup-android-arm-eabi@4.52.2': + '@rollup/rollup-android-arm-eabi@4.53.2': optional: true - '@rollup/rollup-android-arm64@4.52.2': + '@rollup/rollup-android-arm64@4.53.2': optional: true - '@rollup/rollup-darwin-arm64@4.52.2': + '@rollup/rollup-darwin-arm64@4.53.2': optional: true - '@rollup/rollup-darwin-x64@4.52.2': + '@rollup/rollup-darwin-x64@4.53.2': optional: true - '@rollup/rollup-freebsd-arm64@4.52.2': + '@rollup/rollup-freebsd-arm64@4.53.2': optional: true - '@rollup/rollup-freebsd-x64@4.52.2': + '@rollup/rollup-freebsd-x64@4.53.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.52.2': + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.52.2': + '@rollup/rollup-linux-arm-musleabihf@4.53.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.52.2': + '@rollup/rollup-linux-arm64-gnu@4.53.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.52.2': + '@rollup/rollup-linux-arm64-musl@4.53.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.52.2': + '@rollup/rollup-linux-loong64-gnu@4.53.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.52.2': + '@rollup/rollup-linux-ppc64-gnu@4.53.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.52.2': + '@rollup/rollup-linux-riscv64-gnu@4.53.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.52.2': + '@rollup/rollup-linux-riscv64-musl@4.53.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.52.2': + '@rollup/rollup-linux-s390x-gnu@4.53.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.52.2': + '@rollup/rollup-linux-x64-gnu@4.53.2': optional: true - '@rollup/rollup-linux-x64-musl@4.52.2': + '@rollup/rollup-linux-x64-musl@4.53.2': optional: true - '@rollup/rollup-openharmony-arm64@4.52.2': + '@rollup/rollup-openharmony-arm64@4.53.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.52.2': + '@rollup/rollup-win32-arm64-msvc@4.53.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.52.2': + '@rollup/rollup-win32-ia32-msvc@4.53.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.52.2': + '@rollup/rollup-win32-x64-gnu@4.53.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.52.2': + '@rollup/rollup-win32-x64-msvc@4.53.2': optional: true '@rx-state/core@0.1.4(rxjs@7.8.2)': @@ -3601,51 +3761,51 @@ snapshots: '@standard-schema/utils@0.3.0': {} - '@swc/core-darwin-arm64@1.13.19': + '@swc/core-darwin-arm64@1.15.2': optional: true - '@swc/core-darwin-x64@1.13.19': + '@swc/core-darwin-x64@1.15.2': optional: true - '@swc/core-linux-arm-gnueabihf@1.13.19': + '@swc/core-linux-arm-gnueabihf@1.15.2': optional: true - '@swc/core-linux-arm64-gnu@1.13.19': + '@swc/core-linux-arm64-gnu@1.15.2': optional: true - '@swc/core-linux-arm64-musl@1.13.19': + '@swc/core-linux-arm64-musl@1.15.2': optional: true - '@swc/core-linux-x64-gnu@1.13.19': + '@swc/core-linux-x64-gnu@1.15.2': optional: true - '@swc/core-linux-x64-musl@1.13.19': + '@swc/core-linux-x64-musl@1.15.2': optional: true - '@swc/core-win32-arm64-msvc@1.13.19': + '@swc/core-win32-arm64-msvc@1.15.2': optional: true - '@swc/core-win32-ia32-msvc@1.13.19': + '@swc/core-win32-ia32-msvc@1.15.2': optional: true - '@swc/core-win32-x64-msvc@1.13.19': + '@swc/core-win32-x64-msvc@1.15.2': optional: true - '@swc/core@1.13.19': + '@swc/core@1.15.2': dependencies: '@swc/counter': 0.1.3 '@swc/types': 0.1.25 optionalDependencies: - '@swc/core-darwin-arm64': 1.13.19 - '@swc/core-darwin-x64': 1.13.19 - '@swc/core-linux-arm-gnueabihf': 1.13.19 - '@swc/core-linux-arm64-gnu': 1.13.19 - '@swc/core-linux-arm64-musl': 1.13.19 - '@swc/core-linux-x64-gnu': 1.13.19 - '@swc/core-linux-x64-musl': 1.13.19 - '@swc/core-win32-arm64-msvc': 1.13.19 - '@swc/core-win32-ia32-msvc': 1.13.19 - '@swc/core-win32-x64-msvc': 1.13.19 + '@swc/core-darwin-arm64': 1.15.2 + '@swc/core-darwin-x64': 1.15.2 + '@swc/core-linux-arm-gnueabihf': 1.15.2 + '@swc/core-linux-arm64-gnu': 1.15.2 + '@swc/core-linux-arm64-musl': 1.15.2 + '@swc/core-linux-x64-gnu': 1.15.2 + '@swc/core-linux-x64-musl': 1.15.2 + '@swc/core-win32-arm64-msvc': 1.15.2 + '@swc/core-win32-ia32-msvc': 1.15.2 + '@swc/core-win32-x64-msvc': 1.15.2 '@swc/counter@0.1.3': {} @@ -3653,31 +3813,36 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tanstack/query-core@5.90.2': {} + '@tanstack/query-core@5.90.9': {} '@tanstack/query-devtools@5.90.1': {} - '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.2(react@19.1.1))(react@19.1.1)': + '@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.9(react@19.2.0))(react@19.2.0)': dependencies: '@tanstack/query-devtools': 5.90.1 - '@tanstack/react-query': 5.90.2(react@19.1.1) - react: 19.1.1 + '@tanstack/react-query': 5.90.9(react@19.2.0) + react: 19.2.0 - '@tanstack/react-query@5.90.2(react@19.1.1)': + '@tanstack/react-query@5.90.9(react@19.2.0)': dependencies: - '@tanstack/query-core': 5.90.2 - react: 19.1.1 + '@tanstack/query-core': 5.90.9 + react: 19.2.0 - '@tanstack/react-virtual@3.13.12(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': + '@tanstack/react-virtual@3.13.12(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@tanstack/virtual-core': 3.13.12 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) '@tanstack/virtual-core@3.13.12': {} '@types/byte-size@8.1.2': {} + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} '@types/d3-color@3.1.3': {} @@ -3706,6 +3871,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} + '@types/estree-jsx@1.0.5': dependencies: '@types/estree': 1.0.8 @@ -3736,9 +3903,9 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.5.2': + '@types/node@24.10.1': dependencies: - undici-types: 7.12.0 + undici-types: 7.16.0 '@types/normalize-package-data@2.4.4': {} @@ -3746,24 +3913,24 @@ snapshots: '@types/qs@6.14.0': {} - '@types/react-dom@19.1.9(@types/react@19.1.13)': + '@types/react-dom@19.2.3(@types/react@19.2.4)': dependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 '@types/react-router-dom@5.3.3': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.13 + '@types/react': 19.2.4 '@types/react-router': 5.1.20 '@types/react-router@5.1.20': dependencies: '@types/history': 4.7.11 - '@types/react': 19.1.13 + '@types/react': 19.2.4 - '@types/react@19.1.13': + '@types/react@19.2.4': dependencies: - csstype: 3.1.3 + csstype: 3.2.0 '@types/unist@2.0.11': {} @@ -3775,19 +3942,69 @@ snapshots: '@use-gesture/core@10.3.1': {} - '@use-gesture/react@10.3.1(react@19.1.1)': + '@use-gesture/react@10.3.1(react@19.2.0)': dependencies: '@use-gesture/core': 10.3.1 - react: 19.1.1 + react: 19.2.0 - '@vitejs/plugin-react-swc@4.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': + '@vitejs/plugin-react-swc@4.2.2(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': dependencies: - '@rolldown/pluginutils': 1.0.0-beta.35 - '@swc/core': 1.13.19 - vite: 7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + '@rolldown/pluginutils': 1.0.0-beta.47 + '@swc/core': 1.15.2 + vite: 7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) transitivePeerDependencies: - '@swc/helpers' + '@vitest/expect@4.0.14': + dependencies: + '@standard-schema/spec': 1.0.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + chai: 6.2.1 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.14(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1))': + dependencies: + '@vitest/spy': 4.0.14 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + + '@vitest/pretty-format@4.0.14': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.14': + dependencies: + '@vitest/utils': 4.0.14 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.14': + dependencies: + '@vitest/pretty-format': 4.0.14 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.14': {} + + '@vitest/ui@4.0.14(vitest@4.0.14)': + dependencies: + '@vitest/utils': 4.0.14 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vitest: 4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + + '@vitest/utils@4.0.14': + dependencies: + '@vitest/pretty-format': 4.0.14 + tinyrainbow: 3.0.3 + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -3817,19 +4034,21 @@ snapshots: arrify@1.0.1: {} + assertion-error@2.0.1: {} + asynckit@0.4.0: {} - autoprefixer@10.4.21(postcss@8.5.6): + autoprefixer@10.4.22(postcss@8.5.6): dependencies: - browserslist: 4.26.2 - caniuse-lite: 1.0.30001745 - fraction.js: 4.3.7 + browserslist: 4.28.0 + caniuse-lite: 1.0.30001754 + fraction.js: 5.3.4 normalize-range: 0.1.2 picocolors: 1.1.1 postcss: 8.5.6 postcss-value-parser: 4.2.0 - axios@1.12.2: + axios@1.13.2: dependencies: follow-redirects: 1.15.11 form-data: 4.0.4 @@ -3841,13 +4060,13 @@ snapshots: dependencies: '@babel/runtime': 7.28.4 cosmiconfig: 7.1.0 - resolve: 1.22.10 + resolve: 1.22.11 bail@2.0.2: {} balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.7: {} + baseline-browser-mapping@2.8.28: {} bignumber.js@9.3.1: {} @@ -3862,13 +4081,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.26.2: + browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.7 - caniuse-lite: 1.0.30001745 - electron-to-chromium: 1.5.224 - node-releases: 2.0.21 - update-browserslist-db: 1.1.3(browserslist@4.26.2) + baseline-browser-mapping: 2.8.28 + caniuse-lite: 1.0.30001754 + electron-to-chromium: 1.5.252 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) buffer-from@1.1.2: {} @@ -3894,10 +4113,12 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001745: {} + caniuse-lite@1.0.30001754: {} ccount@2.0.1: {} + chai@6.2.1: {} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -4121,7 +4342,7 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 - csstype@3.1.3: {} + csstype@3.2.0: {} d3-array@3.2.4: dependencies: @@ -4167,7 +4388,7 @@ snapshots: dateformat@3.0.3: {} - dayjs@1.11.18: {} + dayjs@1.11.19: {} debug@4.4.3: dependencies: @@ -4230,7 +4451,7 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv@17.2.2: {} + dotenv@17.2.3: {} dotgitignore@2.1.0: dependencies: @@ -4243,7 +4464,7 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - electron-to-chromium@1.5.224: {} + electron-to-chromium@1.5.252: {} emoji-regex@8.0.0: {} @@ -4259,6 +4480,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -4270,36 +4493,36 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-toolkit@1.39.10: {} + es-toolkit@1.41.0: {} - esbuild@0.25.10: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.10 - '@esbuild/android-arm': 0.25.10 - '@esbuild/android-arm64': 0.25.10 - '@esbuild/android-x64': 0.25.10 - '@esbuild/darwin-arm64': 0.25.10 - '@esbuild/darwin-x64': 0.25.10 - '@esbuild/freebsd-arm64': 0.25.10 - '@esbuild/freebsd-x64': 0.25.10 - '@esbuild/linux-arm': 0.25.10 - '@esbuild/linux-arm64': 0.25.10 - '@esbuild/linux-ia32': 0.25.10 - '@esbuild/linux-loong64': 0.25.10 - '@esbuild/linux-mips64el': 0.25.10 - '@esbuild/linux-ppc64': 0.25.10 - '@esbuild/linux-riscv64': 0.25.10 - '@esbuild/linux-s390x': 0.25.10 - '@esbuild/linux-x64': 0.25.10 - '@esbuild/netbsd-arm64': 0.25.10 - '@esbuild/netbsd-x64': 0.25.10 - '@esbuild/openbsd-arm64': 0.25.10 - '@esbuild/openbsd-x64': 0.25.10 - '@esbuild/openharmony-arm64': 0.25.10 - '@esbuild/sunos-x64': 0.25.10 - '@esbuild/win32-arm64': 0.25.10 - '@esbuild/win32-ia32': 0.25.10 - '@esbuild/win32-x64': 0.25.10 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -4309,10 +4532,16 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + eventemitter3@5.0.1: {} events@3.3.0: {} + expect-type@1.2.2: {} + extend@3.0.2: {} fast-deep-equal@3.1.3: {} @@ -4321,6 +4550,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.8.2: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -4351,6 +4582,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + flatted@3.3.3: {} + follow-redirects@1.15.11: {} form-data@4.0.4: @@ -4361,17 +4594,17 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} - framer-motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + framer-motion@12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - motion-dom: 12.23.21 + motion-dom: 12.23.23 motion-utils: 12.23.6 tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) fsevents@2.3.3: optional: true @@ -4437,7 +4670,7 @@ snapshots: dependencies: is-glob: 4.0.3 - globals@16.4.0: {} + globals@16.5.0: {} gopd@1.2.0: {} @@ -4495,7 +4728,7 @@ snapshots: hast-util-from-parse5: 8.0.3 hast-util-to-parse5: 8.0.0 html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 parse5: 7.3.0 unist-util-position: 5.0.0 unist-util-visit: 5.0.0 @@ -4523,7 +4756,7 @@ snapshots: mdast-util-mdxjs-esm: 2.0.1 property-information: 7.1.0 space-separated-tokens: 2.0.2 - style-to-js: 1.1.17 + style-to-js: 1.1.19 unist-util-position: 5.0.0 vfile-message: 4.0.3 transitivePeerDependencies: @@ -4568,15 +4801,15 @@ snapshots: domhandler: 5.0.3 htmlparser2: 10.0.0 - html-react-parser@5.2.6(@types/react@19.1.13)(react@19.1.1): + html-react-parser@5.2.8(@types/react@19.2.4)(react@19.2.0): dependencies: domhandler: 5.0.3 html-dom-parser: 5.1.1 - react: 19.1.1 + react: 19.2.0 react-property: 2.0.2 - style-to-js: 1.1.17 + style-to-js: 1.1.19 optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 html-url-attributes@3.0.1: {} @@ -4591,7 +4824,7 @@ snapshots: humanize-duration@3.33.1: {} - immer@10.1.3: {} + immer@10.2.0: {} immutable@4.3.7: {} @@ -4606,7 +4839,7 @@ snapshots: ini@1.3.8: {} - inline-style-parser@0.2.4: {} + inline-style-parser@0.2.6: {} internmap@2.0.3: {} @@ -4682,9 +4915,9 @@ snapshots: lines-and-columns@1.2.4: {} - little-state-machine@4.8.1(react@19.1.1): + little-state-machine@4.8.1(react@19.2.0): dependencies: - react: 19.1.1 + react: 19.2.0 load-json-file@4.0.0: dependencies: @@ -4731,6 +4964,10 @@ snapshots: dependencies: yallist: 4.0.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -4796,9 +5033,9 @@ snapshots: mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 - mdast-util-to-hast@13.2.0: + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 @@ -4840,9 +5077,9 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 - merge-refs@2.0.0(@types/react@19.1.13): + merge-refs@2.0.0(@types/react@19.2.4): optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 micromark-core-commonmark@2.0.3: dependencies: @@ -5003,20 +5240,22 @@ snapshots: modify-values@1.0.1: {} - motion-dom@12.23.21: + motion-dom@12.23.23: dependencies: motion-utils: 12.23.6 motion-utils@12.23.6: {} - motion@12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + motion@12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - framer-motion: 12.23.22(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + framer-motion: 12.23.24(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) tslib: 2.8.1 optionalDependencies: '@emotion/is-prop-valid': 1.4.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + + mrmime@2.0.1: {} ms@2.1.3: {} @@ -5026,12 +5265,12 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.21: {} + node-releases@2.0.27: {} normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 - resolve: 1.22.10 + resolve: 1.22.11 semver: 5.7.2 validate-npm-package-license: 3.0.4 @@ -5039,7 +5278,7 @@ snapshots: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.16.1 - semver: 7.7.2 + semver: 7.7.3 validate-npm-package-license: 3.0.4 normalize-path@3.0.0: {} @@ -5054,6 +5293,8 @@ snapshots: object-inspect@1.13.4: {} + obug@2.1.1: {} + p-limit@1.3.0: dependencies: p-try: 1.0.0 @@ -5128,6 +5369,8 @@ snapshots: path-type@4.0.0: {} + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5184,57 +5427,57 @@ snapshots: radash@12.1.1: {} - react-click-away-listener@2.4.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-click-away-listener@2.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-datepicker@8.7.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-datepicker@8.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.1.1(react@19.1.1))(react@19.1.1) + '@floating-ui/react': 0.27.16(react-dom@19.2.0(react@19.2.0))(react@19.2.0) clsx: 2.1.1 date-fns: 4.1.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-dom@19.1.1(react@19.1.1): + react-dom@19.2.0(react@19.2.0): dependencies: - react: 19.1.1 - scheduler: 0.26.0 + react: 19.2.0 + scheduler: 0.27.0 - react-hook-form@7.63.0(react@19.1.1): + react-hook-form@7.66.0(react@19.2.0): dependencies: - react: 19.1.1 + react: 19.2.0 - react-idle-timer@5.7.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-idle-timer@5.7.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react-intersection-observer@9.16.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-intersection-observer@9.16.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.1 + react: 19.2.0 optionalDependencies: - react-dom: 19.1.1(react@19.1.1) + react-dom: 19.2.0(react@19.2.0) react-is@16.13.1: {} - react-is@19.1.1: {} + react-is@19.2.0: {} - react-loading-skeleton@3.5.0(react@19.1.1): + react-loading-skeleton@3.5.0(react@19.2.0): dependencies: - react: 19.1.1 + react: 19.2.0 - react-markdown@10.1.0(@types/react@19.1.13)(react@19.1.1): + react-markdown@10.1.0(@types/react@19.2.4)(react@19.2.0): dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@types/react': 19.1.13 + '@types/react': 19.2.4 devlop: 1.1.0 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 - mdast-util-to-hast: 13.2.0 - react: 19.1.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.0 remark-parse: 11.0.0 remark-rehype: 11.1.2 unified: 11.0.5 @@ -5245,55 +5488,55 @@ snapshots: react-property@2.0.2: {} - react-qr-code@2.0.18(react@19.1.1): + react-qr-code@2.0.18(react@19.2.0): dependencies: prop-types: 15.8.1 qr.js: 0.0.0 - react: 19.1.1 + react: 19.2.0 - react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.1.1 - use-sync-external-store: 1.5.0(react@19.1.1) + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) optionalDependencies: - '@types/react': 19.1.13 + '@types/react': 19.2.4 redux: 5.0.1 - react-resize-detector@12.3.0(react@19.1.1): + react-resize-detector@12.3.0(react@19.2.0): dependencies: - es-toolkit: 1.39.10 - react: 19.1.1 + es-toolkit: 1.41.0 + react: 19.2.0 - react-router-dom@6.30.1(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-router-dom@6.30.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-router: 6.30.1(react@19.1.1) + '@remix-run/router': 1.23.1 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-router: 6.30.2(react@19.2.0) - react-router@6.30.1(react@19.1.1): + react-router@6.30.2(react@19.2.0): dependencies: - '@remix-run/router': 1.23.0 - react: 19.1.1 + '@remix-run/router': 1.23.1 + react: 19.2.0 - react-simple-animate@3.5.3(react-dom@19.1.1(react@19.1.1)): + react-simple-animate@3.5.3(react-dom@19.2.0(react@19.2.0)): dependencies: - react-dom: 19.1.1(react@19.1.1) + react-dom: 19.2.0(react@19.2.0) - react-tracked@2.0.1(react@19.1.1)(scheduler@0.26.0): + react-tracked@2.0.1(react@19.2.0)(scheduler@0.26.0): dependencies: proxy-compare: 3.0.1 - react: 19.1.1 + react: 19.2.0 scheduler: 0.26.0 - use-context-selector: 2.0.0(react@19.1.1)(scheduler@0.26.0) + use-context-selector: 2.0.0(react@19.2.0)(scheduler@0.26.0) - react-virtualized-auto-sizer@1.0.26(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + react-virtualized-auto-sizer@1.0.26(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - react@19.1.1: {} + react@19.2.0: {} read-pkg-up@3.0.0: dependencies: @@ -5339,21 +5582,21 @@ snapshots: dependencies: picomatch: 2.3.1 - recharts@3.2.1(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.1))(react-is@19.1.1)(react@19.1.1)(redux@5.0.1): + recharts@3.4.1(@types/react@19.2.4)(react-dom@19.2.0(react@19.2.0))(react-is@19.2.0)(react@19.2.0)(redux@5.0.1): dependencies: - '@reduxjs/toolkit': 2.9.0(react-redux@9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1))(react@19.1.1) + '@reduxjs/toolkit': 2.10.1(react-redux@9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1))(react@19.2.0) clsx: 2.1.1 decimal.js-light: 2.5.1 - es-toolkit: 1.39.10 + es-toolkit: 1.41.0 eventemitter3: 5.0.1 - immer: 10.1.3 - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) - react-is: 19.1.1 - react-redux: 9.2.0(@types/react@19.1.13)(react@19.1.1)(redux@5.0.1) + immer: 10.2.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 19.2.0 + react-redux: 9.2.0(@types/react@19.2.4)(react@19.2.0)(redux@5.0.1) reselect: 5.1.1 tiny-invariant: 1.3.3 - use-sync-external-store: 1.5.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.2.0) victory-vendor: 37.3.6 transitivePeerDependencies: - '@types/react' @@ -5403,7 +5646,7 @@ snapshots: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - mdast-util-to-hast: 13.2.0 + mdast-util-to-hast: 13.2.1 unified: 11.0.5 vfile: 6.0.3 @@ -5415,38 +5658,38 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.10: + resolve@1.22.11: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rollup@4.52.2: + rollup@4.53.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.52.2 - '@rollup/rollup-android-arm64': 4.52.2 - '@rollup/rollup-darwin-arm64': 4.52.2 - '@rollup/rollup-darwin-x64': 4.52.2 - '@rollup/rollup-freebsd-arm64': 4.52.2 - '@rollup/rollup-freebsd-x64': 4.52.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.52.2 - '@rollup/rollup-linux-arm-musleabihf': 4.52.2 - '@rollup/rollup-linux-arm64-gnu': 4.52.2 - '@rollup/rollup-linux-arm64-musl': 4.52.2 - '@rollup/rollup-linux-loong64-gnu': 4.52.2 - '@rollup/rollup-linux-ppc64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-gnu': 4.52.2 - '@rollup/rollup-linux-riscv64-musl': 4.52.2 - '@rollup/rollup-linux-s390x-gnu': 4.52.2 - '@rollup/rollup-linux-x64-gnu': 4.52.2 - '@rollup/rollup-linux-x64-musl': 4.52.2 - '@rollup/rollup-openharmony-arm64': 4.52.2 - '@rollup/rollup-win32-arm64-msvc': 4.52.2 - '@rollup/rollup-win32-ia32-msvc': 4.52.2 - '@rollup/rollup-win32-x64-gnu': 4.52.2 - '@rollup/rollup-win32-x64-msvc': 4.52.2 + '@rollup/rollup-android-arm-eabi': 4.53.2 + '@rollup/rollup-android-arm64': 4.53.2 + '@rollup/rollup-darwin-arm64': 4.53.2 + '@rollup/rollup-darwin-x64': 4.53.2 + '@rollup/rollup-freebsd-arm64': 4.53.2 + '@rollup/rollup-freebsd-x64': 4.53.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.2 + '@rollup/rollup-linux-arm-musleabihf': 4.53.2 + '@rollup/rollup-linux-arm64-gnu': 4.53.2 + '@rollup/rollup-linux-arm64-musl': 4.53.2 + '@rollup/rollup-linux-loong64-gnu': 4.53.2 + '@rollup/rollup-linux-ppc64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-musl': 4.53.2 + '@rollup/rollup-linux-s390x-gnu': 4.53.2 + '@rollup/rollup-linux-x64-gnu': 4.53.2 + '@rollup/rollup-linux-x64-musl': 4.53.2 + '@rollup/rollup-openharmony-arm64': 4.53.2 + '@rollup/rollup-win32-arm64-msvc': 4.53.2 + '@rollup/rollup-win32-ia32-msvc': 4.53.2 + '@rollup/rollup-win32-x64-gnu': 4.53.2 + '@rollup/rollup-win32-x64-msvc': 4.53.2 fsevents: 2.3.3 rxjs@7.8.2: @@ -5465,11 +5708,13 @@ snapshots: scheduler@0.26.0: {} + scheduler@0.27.0: {} + semver@5.7.2: {} semver@6.3.1: {} - semver@7.7.2: {} + semver@7.7.3: {} set-blocking@2.0.0: {} @@ -5503,6 +5748,14 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5539,6 +5792,8 @@ snapshots: dependencies: through: 2.3.8 + stackback@0.0.2: {} + standard-version@9.5.0: dependencies: chalk: 2.4.2 @@ -5552,10 +5807,12 @@ snapshots: figures: 3.2.0 find-up: 5.0.0 git-semver-tags: 4.1.1 - semver: 7.7.2 + semver: 7.7.3 stringify-package: 1.0.1 yargs: 16.2.0 + std-env@3.10.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -5587,13 +5844,13 @@ snapshots: dependencies: min-indent: 1.0.1 - style-to-js@1.1.17: + style-to-js@1.1.19: dependencies: - style-to-object: 1.0.9 + style-to-object: 1.0.12 - style-to-object@1.0.9: + style-to-object@1.0.12: dependencies: - inline-style-parser: 0.2.4 + inline-style-parser: 0.2.6 stylis@4.2.0: {} @@ -5611,7 +5868,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - tabbable@6.2.0: {} + tabbable@6.3.0: {} terser@5.37.0: dependencies: @@ -5729,15 +5986,23 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + totalist@3.0.1: {} + tree-kill@1.2.2: {} trim-lines@3.0.1: {} @@ -5758,16 +6023,16 @@ snapshots: typedarray@0.0.6: {} - typesafe-i18n@5.26.2(typescript@5.9.2): + typesafe-i18n@5.26.2(typescript@5.9.3): dependencies: - typescript: 5.9.2 + typescript: 5.9.3 - typescript@5.9.2: {} + typescript@5.9.3: {} uglify-js@3.19.3: optional: true - undici-types@7.12.0: {} + undici-types@7.16.0: {} unified@11.0.5: dependencies: @@ -5779,7 +6044,7 @@ snapshots: trough: 2.2.0 vfile: 6.0.3 - unist-util-is@6.0.0: + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -5791,42 +6056,42 @@ snapshots: dependencies: '@types/unist': 3.0.3 - unist-util-visit-parents@6.0.1: + unist-util-visit-parents@6.0.2: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 + unist-util-is: 6.0.1 unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 - unist-util-is: 6.0.0 - unist-util-visit-parents: 6.0.1 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 - update-browserslist-db@1.1.3(browserslist@4.26.2): + update-browserslist-db@1.1.4(browserslist@4.28.0): dependencies: - browserslist: 4.26.2 + browserslist: 4.28.0 escalade: 3.2.0 picocolors: 1.1.1 - use-breakpoint@4.0.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1): + use-breakpoint@4.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: - react: 19.1.1 - react-dom: 19.1.1(react@19.1.1) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) - use-context-selector@2.0.0(react@19.1.1)(scheduler@0.26.0): + use-context-selector@2.0.0(react@19.2.0)(scheduler@0.26.0): dependencies: - react: 19.1.1 + react: 19.2.0 scheduler: 0.26.0 - use-deep-compare-effect@1.8.1(react@19.1.1): + use-deep-compare-effect@1.8.1(react@19.2.0): dependencies: '@babel/runtime': 7.28.4 dequal: 2.0.3 - react: 19.1.1 + react: 19.2.0 - use-sync-external-store@1.5.0(react@19.1.1): + use-sync-external-store@1.6.0(react@19.2.0): dependencies: - react: 19.1.1 + react: 19.2.0 util-deprecate@1.0.2: {} @@ -5869,30 +6134,73 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vite-plugin-package-version@1.1.0(vite@7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): + vite-plugin-package-version@1.1.0(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)): dependencies: - vite: 7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + vite: 7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) - vite@7.1.7(@types/node@24.5.2)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): + vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): dependencies: - esbuild: 0.25.10 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.52.2 + rollup: 4.53.2 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.5.2 + '@types/node': 24.10.1 fsevents: 2.3.3 jiti: 2.4.2 sass: 1.70.0 terser: 5.37.0 yaml: 2.6.1 + vitest@4.0.14(@types/node@24.10.1)(@vitest/ui@4.0.14)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1): + dependencies: + '@vitest/expect': 4.0.14 + '@vitest/mocker': 4.0.14(vite@7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1)) + '@vitest/pretty-format': 4.0.14 + '@vitest/runner': 4.0.14 + '@vitest/snapshot': 4.0.14 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.2.2(@types/node@24.10.1)(jiti@2.4.2)(sass@1.70.0)(terser@5.37.0)(yaml@2.6.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.10.1 + '@vitest/ui': 4.0.14(vitest@4.0.14) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + web-namespaces@2.0.1: {} which-module@2.0.1: {} + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wordwrap@1.0.0: {} wrap-ansi@6.2.0: @@ -5969,11 +6277,11 @@ snapshots: zod@3.25.76: {} - zustand@5.0.8(@types/react@19.1.13)(immer@10.1.3)(react@19.1.1)(use-sync-external-store@1.5.0(react@19.1.1)): + zustand@5.0.8(@types/react@19.2.4)(immer@10.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): optionalDependencies: - '@types/react': 19.1.13 - immer: 10.1.3 - react: 19.1.1 - use-sync-external-store: 1.5.0(react@19.1.1) + '@types/react': 19.2.4 + immer: 10.2.0 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) zwitch@2.0.4: {} diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index b8a7a44721..fa88c26ca5 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -1162,6 +1162,14 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do external: 'External MFA', }, }, + serviceLocationModeSelect: { + label: 'Service Location Mode', + options: { + disabled: 'Disabled', + prelogon: 'Pre-logon', + alwayson: 'Always-on', + }, + }, }, settingsPage: { title: 'Settings', @@ -1380,6 +1388,10 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do enable_directory_sync: { label: 'Enable directory synchronization', }, + prefetch_users: { + label: 'Prefetch users', + helper: 'Fetch users from external provider and create user accounts in Defguard without waiting for them to log in', + }, sync_target: { label: 'Synchronize', helper: @@ -1696,16 +1708,29 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do helper: "When this option is enabled, only users in the Admin group can manage devices in user profile (it's disabled for all other users)", }, - disableAllTraffic: { - label: 'Disable the option to route all traffic through VPN', - helper: - 'When this option is enabled, users will not be able to route all traffic through the VPN using the defguard client.', - }, manualConfig: { label: "Disable users' ability to manually configure WireGuard client", helper: "When this option is enabled, users won't be able to view or download configuration for the manual WireGuard client setup. Only the Defguard desktop client configuration will be available.", }, + clientTrafficPolicy: { + header: 'Client traffic policy', + none: { + label: 'None', + helper: + 'None - When this option is enabled, users will be able to select all routing options.', + }, + disableAllTraffic: { + label: 'Disable the option to route all traffic through VPN', + helper: + 'Disable all traffic - When this option is enabled, users will not be able to route all traffic through the VPN.', + }, + forceAllTraffic: { + label: 'Force the clients to route all traffic through VPN', + helper: + 'Force all traffic - When this option is enabled, the users will always route all traffic through the VPN.', + }, + }, }, }, gatewayNotifications: { @@ -2030,6 +2055,17 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do "Internal MFA - MFA is enforced using Defguard's built-in MFA (e.g. TOTP, WebAuthn) with internal identity", external: 'External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA', + serviceLocationWarning: + "Location MFA can't be used when service location mode is enabled.", + }, + serviceLocation: { + description: + 'Choose if this location should work as a service location. This feature is currently not supported on every platform. Consult our [documentation](https://docs.defguard.net/features/service-locations) for more details.', + preLogon: + 'Pre-logon - A VPN connection to this location will be active only before the user logs in on their device. When the user completes the login, the VPN connection will be terminated.', + alwaysOn: + 'Always-on - A VPN connection will always be active when the user device is on.', + mfaWarning: "Service locations can't be used while location MFA is enabled.", }, }, sections: { @@ -2039,6 +2075,9 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do mfa: { header: 'Multi-Factor Authentication', }, + serviceLocation: { + header: 'Service location', + }, }, messages: { networkModified: 'Location modified.', @@ -2082,6 +2121,9 @@ Licensing information: [https://docs.defguard.net/enterprise/license](https://do location_mfa_mode: { label: 'MFA requirement', }, + service_location_mode: { + label: 'Service location mode', + }, }, controls: { submit: 'Save changes', diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 2bd9e20581..a2ce0a2129 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -2851,6 +2851,26 @@ type RootTranslation = { external: string } } + serviceLocationModeSelect: { + /** + * S​e​r​v​i​c​e​ ​L​o​c​a​t​i​o​n​ ​M​o​d​e + */ + label: string + options: { + /** + * D​i​s​a​b​l​e​d + */ + disabled: string + /** + * P​r​e​-​l​o​g​o​n + */ + prelogon: string + /** + * A​l​w​a​y​s​-​o​n + */ + alwayson: string + } + } } settingsPage: { /** @@ -3396,6 +3416,16 @@ type RootTranslation = { */ label: string } + prefetch_users: { + /** + * P​r​e​f​e​t​c​h​ ​u​s​e​r​s + */ + label: string + /** + * F​e​t​c​h​ ​u​s​e​r​s​ ​f​r​o​m​ ​e​x​t​e​r​n​a​l​ ​p​r​o​v​i​d​e​r​ ​a​n​d​ ​c​r​e​a​t​e​ ​u​s​e​r​ ​a​c​c​o​u​n​t​s​ ​i​n​ ​D​e​f​g​u​a​r​d​ ​w​i​t​h​o​u​t​ ​w​a​i​t​i​n​g​ ​f​o​r​ ​t​h​e​m​ ​t​o​ ​l​o​g​ ​i​n + */ + helper: string + } sync_target: { /** * S​y​n​c​h​r​o​n​i​z​e @@ -4052,16 +4082,6 @@ type RootTranslation = { */ helper: string } - disableAllTraffic: { - /** - * D​i​s​a​b​l​e​ ​t​h​e​ ​o​p​t​i​o​n​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N - */ - label: string - /** - * W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​ ​u​s​i​n​g​ ​t​h​e​ ​d​e​f​g​u​a​r​d​ ​c​l​i​e​n​t​. - */ - helper: string - } manualConfig: { /** * D​i​s​a​b​l​e​ ​u​s​e​r​s​'​ ​a​b​i​l​i​t​y​ ​t​o​ ​m​a​n​u​a​l​l​y​ ​c​o​n​f​i​g​u​r​e​ ​W​i​r​e​G​u​a​r​d​ ​c​l​i​e​n​t @@ -4072,6 +4092,42 @@ type RootTranslation = { */ helper: string } + clientTrafficPolicy: { + /** + * C​l​i​e​n​t​ ​t​r​a​f​f​i​c​ ​p​o​l​i​c​y + */ + header: string + none: { + /** + * N​o​n​e + */ + label: string + /** + * N​o​n​e​ ​-​ ​W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​b​e​ ​a​b​l​e​ ​t​o​ ​s​e​l​e​c​t​ ​a​l​l​ ​r​o​u​t​i​n​g​ ​o​p​t​i​o​n​s​. + */ + helper: string + } + disableAllTraffic: { + /** + * D​i​s​a​b​l​e​ ​t​h​e​ ​o​p​t​i​o​n​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N + */ + label: string + /** + * D​i​s​a​b​l​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​-​ ​W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​u​s​e​r​s​ ​w​i​l​l​ ​n​o​t​ ​b​e​ ​a​b​l​e​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​. + */ + helper: string + } + forceAllTraffic: { + /** + * F​o​r​c​e​ ​t​h​e​ ​c​l​i​e​n​t​s​ ​t​o​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​V​P​N + */ + label: string + /** + * F​o​r​c​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​-​ ​W​h​e​n​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​i​s​ ​e​n​a​b​l​e​d​,​ ​t​h​e​ ​u​s​e​r​s​ ​w​i​l​l​ ​a​l​w​a​y​s​ ​r​o​u​t​e​ ​a​l​l​ ​t​r​a​f​f​i​c​ ​t​h​r​o​u​g​h​ ​t​h​e​ ​V​P​N​. + */ + helper: string + } + } } } gatewayNotifications: { @@ -4871,6 +4927,28 @@ type RootTranslation = { * E​x​t​e​r​n​a​l​ ​M​F​A​ ​-​ ​I​f​ ​c​o​n​f​i​g​u​r​e​d​ ​(​s​e​e​ ​[​O​p​e​n​I​D​ ​s​e​t​t​i​n​g​s​]​(​s​e​t​t​i​n​g​s​)​)​ ​t​h​i​s​ ​o​p​t​i​o​n​ ​u​s​e​s​ ​e​x​t​e​r​n​a​l​ ​i​d​e​n​t​i​t​y​ ​p​r​o​v​i​d​e​r​ ​f​o​r​ ​M​F​A */ external: string + /** + * L​o​c​a​t​i​o​n​ ​M​F​A​ ​c​a​n​'​t​ ​b​e​ ​u​s​e​d​ ​w​h​e​n​ ​s​e​r​v​i​c​e​ ​l​o​c​a​t​i​o​n​ ​m​o​d​e​ ​i​s​ ​e​n​a​b​l​e​d​. + */ + serviceLocationWarning: string + } + serviceLocation: { + /** + * C​h​o​o​s​e​ ​i​f​ ​t​h​i​s​ ​l​o​c​a​t​i​o​n​ ​s​h​o​u​l​d​ ​w​o​r​k​ ​a​s​ ​a​ ​s​e​r​v​i​c​e​ ​l​o​c​a​t​i​o​n​.​ ​T​h​i​s​ ​f​e​a​t​u​r​e​ ​i​s​ ​c​u​r​r​e​n​t​l​y​ ​n​o​t​ ​s​u​p​p​o​r​t​e​d​ ​o​n​ ​e​v​e​r​y​ ​p​l​a​t​f​o​r​m​.​ ​C​o​n​s​u​l​t​ ​o​u​r​ ​[​d​o​c​u​m​e​n​t​a​t​i​o​n​]​(​h​t​t​p​s​:​/​/​d​o​c​s​.​d​e​f​g​u​a​r​d​.​n​e​t​/​f​e​a​t​u​r​e​s​/​s​e​r​v​i​c​e​-​l​o​c​a​t​i​o​n​s​)​ ​f​o​r​ ​m​o​r​e​ ​d​e​t​a​i​l​s​. + */ + description: string + /** + * P​r​e​-​l​o​g​o​n​ ​-​ ​A​ ​V​P​N​ ​c​o​n​n​e​c​t​i​o​n​ ​t​o​ ​t​h​i​s​ ​l​o​c​a​t​i​o​n​ ​w​i​l​l​ ​b​e​ ​a​c​t​i​v​e​ ​o​n​l​y​ ​b​e​f​o​r​e​ ​t​h​e​ ​u​s​e​r​ ​l​o​g​s​ ​i​n​ ​o​n​ ​t​h​e​i​r​ ​d​e​v​i​c​e​.​ ​W​h​e​n​ ​t​h​e​ ​u​s​e​r​ ​c​o​m​p​l​e​t​e​s​ ​t​h​e​ ​l​o​g​i​n​,​ ​t​h​e​ ​V​P​N​ ​c​o​n​n​e​c​t​i​o​n​ ​w​i​l​l​ ​b​e​ ​t​e​r​m​i​n​a​t​e​d​. + */ + preLogon: string + /** + * A​l​w​a​y​s​-​o​n​ ​-​ ​A​ ​V​P​N​ ​c​o​n​n​e​c​t​i​o​n​ ​w​i​l​l​ ​a​l​w​a​y​s​ ​b​e​ ​a​c​t​i​v​e​ ​w​h​e​n​ ​t​h​e​ ​u​s​e​r​ ​d​e​v​i​c​e​ ​i​s​ ​o​n​. + */ + alwaysOn: string + /** + * S​e​r​v​i​c​e​ ​l​o​c​a​t​i​o​n​s​ ​c​a​n​'​t​ ​b​e​ ​u​s​e​d​ ​w​h​i​l​e​ ​l​o​c​a​t​i​o​n​ ​M​F​A​ ​i​s​ ​e​n​a​b​l​e​d​. + */ + mfaWarning: string } } sections: { @@ -4886,6 +4964,12 @@ type RootTranslation = { */ header: string } + serviceLocation: { + /** + * S​e​r​v​i​c​e​ ​l​o​c​a​t​i​o​n + */ + header: string + } } messages: { /** @@ -4974,6 +5058,12 @@ type RootTranslation = { */ label: string } + service_location_mode: { + /** + * S​e​r​v​i​c​e​ ​l​o​c​a​t​i​o​n​ ​m​o​d​e + */ + label: string + } } controls: { /** @@ -9525,6 +9615,26 @@ export type TranslationFunctions = { external: () => LocalizedString } } + serviceLocationModeSelect: { + /** + * Service Location Mode + */ + label: () => LocalizedString + options: { + /** + * Disabled + */ + disabled: () => LocalizedString + /** + * Pre-logon + */ + prelogon: () => LocalizedString + /** + * Always-on + */ + alwayson: () => LocalizedString + } + } } settingsPage: { /** @@ -10067,6 +10177,16 @@ export type TranslationFunctions = { */ label: () => LocalizedString } + prefetch_users: { + /** + * Prefetch users + */ + label: () => LocalizedString + /** + * Fetch users from external provider and create user accounts in Defguard without waiting for them to log in + */ + helper: () => LocalizedString + } sync_target: { /** * Synchronize @@ -10719,16 +10839,6 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } - disableAllTraffic: { - /** - * Disable the option to route all traffic through VPN - */ - label: () => LocalizedString - /** - * When this option is enabled, users will not be able to route all traffic through the VPN using the defguard client. - */ - helper: () => LocalizedString - } manualConfig: { /** * Disable users' ability to manually configure WireGuard client @@ -10739,6 +10849,42 @@ export type TranslationFunctions = { */ helper: () => LocalizedString } + clientTrafficPolicy: { + /** + * Client traffic policy + */ + header: () => LocalizedString + none: { + /** + * None + */ + label: () => LocalizedString + /** + * None - When this option is enabled, users will be able to select all routing options. + */ + helper: () => LocalizedString + } + disableAllTraffic: { + /** + * Disable the option to route all traffic through VPN + */ + label: () => LocalizedString + /** + * Disable all traffic - When this option is enabled, users will not be able to route all traffic through the VPN. + */ + helper: () => LocalizedString + } + forceAllTraffic: { + /** + * Force the clients to route all traffic through VPN + */ + label: () => LocalizedString + /** + * Force all traffic - When this option is enabled, the users will always route all traffic through the VPN. + */ + helper: () => LocalizedString + } + } } } gatewayNotifications: { @@ -11526,6 +11672,28 @@ export type TranslationFunctions = { * External MFA - If configured (see [OpenID settings](settings)) this option uses external identity provider for MFA */ external: () => LocalizedString + /** + * Location MFA can't be used when service location mode is enabled. + */ + serviceLocationWarning: () => LocalizedString + } + serviceLocation: { + /** + * Choose if this location should work as a service location. This feature is currently not supported on every platform. Consult our [documentation](https://docs.defguard.net/features/service-locations) for more details. + */ + description: () => LocalizedString + /** + * Pre-logon - A VPN connection to this location will be active only before the user logs in on their device. When the user completes the login, the VPN connection will be terminated. + */ + preLogon: () => LocalizedString + /** + * Always-on - A VPN connection will always be active when the user device is on. + */ + alwaysOn: () => LocalizedString + /** + * Service locations can't be used while location MFA is enabled. + */ + mfaWarning: () => LocalizedString } } sections: { @@ -11541,6 +11709,12 @@ export type TranslationFunctions = { */ header: () => LocalizedString } + serviceLocation: { + /** + * Service location + */ + header: () => LocalizedString + } } messages: { /** @@ -11629,6 +11803,12 @@ export type TranslationFunctions = { */ label: () => LocalizedString } + service_location_mode: { + /** + * Service location mode + */ + label: () => LocalizedString + } } controls: { /** diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 588a4a60e7..3255916235 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -1032,6 +1032,22 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe contact: 'poprzez kontakt:', }, }, + locationMfaModeSelect: { + label: 'Wymaganie MFA', + options: { + disabled: 'Nie wymuszaj MFA', + internal: 'Wewnętrzne MFA', + external: 'Zewnętrzne MFA', + }, + }, + serviceLocationModeSelect: { + label: 'Tryb lokalizacji usługi', + options: { + disabled: 'Wyłączone', + prelogon: 'Pre-logon', + alwayson: 'Always-on', + }, + }, }, settingsPage: { title: 'Ustawienia', @@ -1478,16 +1494,29 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe helper: 'Kiedy ta opcja jest włączona, tylko użytkownicy w grupie "Admin" mogą zarządzać urządzeniami w profilu użytkownika', }, - disableAllTraffic: { - label: 'Zablokuj możliwość przekierowania całego ruchu przez VPN', - helper: - 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli przekierować całego ruchu przez VPN za pomocą klienta Defguard.', - }, manualConfig: { label: 'Wyłącz manualną konfigurację WireGuard', helper: 'Kiedy ta opcja jest włączona, użytkownicy nie będą mogli pobrać ani wyświetlić danych do manualnej konfiguracji WireGuard. Możliwe będzie wyłącznie skonfigurowanie klienta Defguard.', }, + clientTrafficPolicy: { + header: 'Polityka przekierowania ruchu klientów', + none: { + label: 'Brak', + helper: + 'Brak - Kiedy ta opcja jest włączona, użytkownicy mogą wybrać dowolny typ przekierowania ruchu.', + }, + disableAllTraffic: { + label: 'Zablokuj możliwość przekierowania całego ruchu przez VPN', + helper: + 'Zablokuj przekierowanie całego ruchu - Kiedy ta opcja jest włączona, użytkownicy nie będą mogli przekierować całego ruchu przez VPN.', + }, + forceAllTraffic: { + label: 'Wymuś przekierowanie całego ruchu przez VPN', + helper: + 'Wymuś przekierowanie całego ruchu - Kiedy ta opcja jest włączona, użytkownicy będą zawsze przekierowywać cały ruch przez VPN.', + }, + } }, }, gatewayNotifications: { @@ -1784,11 +1813,46 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe allowedIps: 'Lista adresów/masek, które powinny być routowane przez sieć VPN.', allowedGroups: 'Domyślnie wszyscy użytkownicy będą mogli połączyć się z tą lokalizacją. Jeżeli chcesz ogranicznyć dostęp do tej lokalizacji do wybranej grupy użytkowników, wybierz ją poniżej.', + aclFeatureDisabled: + 'Funkcjonalność ACL jest funkcją enterprise i przekroczyłeś limity użytkowników, urządzeń lub sieciź. Aby korzystać z tej funkcji, kup licencję enterprise lub zaktualizuj istniejącą.', + peerDisconnectThreshold: + 'Klienci autoryzowani za pomocą MFA zostaną rozłączeni z lokalizacji, gdy nie zostanie wykryta żadna aktywność sieciowa między nimi a bramą VPN przez czas skonfigurowany poniżej.', + locationMfaMode: { + description: + 'Wybierz, w jaki sposób wymuszane jest MFA podczas łączenia się z tą lokalizacją:', + internal: + 'Wewnętrzne MFA - MFA jest wymuszane przy użyciu wbudowanego MFA Defguard (np. TOTP, WebAuthn) z wewnętrzną tożsamością', + external: + 'Zewnętrzne MFA - Jeśli skonfigurowane (zobacz [ustawienia OpenID](settings)), ta opcja używa zewnętrznego dostawcy tożsamości do MFA', + serviceLocationWarning: + 'Nie można używać MFA lokalizacji, gdy włączony jest tryb lokalizacji serwisowej.', + }, + serviceLocation: { + description: + 'Wybierz, czy ta lokalizacja ma działać jako lokalizacja serwisowa. Ta funkcja nie jest obecnie obsługiwana na każdej platformie. Zapoznaj się z naszą [dokumentacją](https://docs.defguard.net/features/service-locations), aby uzyskać więcej szczegółów.', + preLogon: + 'Pre-logon - Połączenie VPN z tą lokalizacją będzie aktywne tylko przed zalogowaniem użytkownika na jego urządzeniu. Połączenie VPN zostanie zakończone po zalogowaniu się użytkownika.', + alwaysOn: + 'Always-on - Połączenie VPN będzie zawsze aktywne, gdy urządzenie użytkownika jest włączone.', + mfaWarning: + 'Nie można używać lokalizacji serwisowej, gdy włączone jest MFA lokalizacji.', + }, }, messages: { networkModified: 'Lokalizacja zmodyfikowana', networkCreated: 'Lokalizacja utworzona', }, + sections: { + accessControl: { + header: 'Kontrola dostępu i firewall', + }, + mfa: { + header: 'Uwierzytelnianie wieloskładnikowe', + }, + serviceLocation: { + header: 'Lokalizacja serwisowa', + }, + }, fields: { name: { label: 'Nazwa lokalizacji', @@ -1827,6 +1891,12 @@ Uwaga, podane tutaj konfiguracje nie posiadają klucza prywatnego. Musisz uzupe acl_default_allow: { label: 'Domyślna polityka ACL', }, + location_mfa_mode: { + label: 'Tryb MFA lokalizacji', + }, + service_location_mode: { + label: 'Tryb lokalizacji serwisowej', + }, }, controls: { submit: 'Zapisz zmiany', diff --git a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx index a054a9150c..ef6b9f0853 100644 --- a/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx +++ b/web/src/pages/devices/modals/AddStandaloneDeviceModal/steps/MethodStep/MethodStep.tsx @@ -88,11 +88,13 @@ export const MethodStep = () => { // biome-ignore lint/correctness/useExhaustiveDependencies: migration, checkMeLater useEffect(() => { if (networks) { - const options: SelectOption[] = networks.map((n) => ({ - key: n.id, - value: n.id, - label: n.name, - })); + const options: SelectOption[] = networks + .filter((n) => n.location_mfa_mode === 'disabled') + .map((n) => ({ + key: n.id, + value: n.id, + label: n.name, + })); setState({ networks, networkOptions: options, diff --git a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx index f73bcecd00..8968939c1e 100644 --- a/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx +++ b/web/src/pages/network/NetworkEditForm/NetworkEditForm.tsx @@ -12,6 +12,7 @@ import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../i18n/i18n-react'; import { FormAclDefaultPolicy } from '../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx'; import { FormLocationMfaModeSelect } from '../../../shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx'; +import { FormServiceLocationModeSelect } from '../../../shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect.tsx'; import { RenderMarkdown } from '../../../shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx'; import { FormCheckBox } from '../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx'; import { FormInput } from '../../../shared/defguard-ui/components/Form/FormInput/FormInput'; @@ -23,14 +24,14 @@ import { useAppStore } from '../../../shared/hooks/store/useAppStore.ts'; import useApi from '../../../shared/hooks/useApi'; import { useToaster } from '../../../shared/hooks/useToaster'; import { QueryKeys } from '../../../shared/queries'; -import { LocationMfaMode, type Network } from '../../../shared/types'; +import { + LocationMfaMode, + type Network, + ServiceLocationMode, +} from '../../../shared/types'; import { titleCase } from '../../../shared/utils/titleCase'; import { trimObjectStrings } from '../../../shared/utils/trimObjectStrings.ts'; -import { - validateIpList, - validateIpOrDomain, - validateIpOrDomainList, -} from '../../../shared/validators'; +import { Validate } from '../../../shared/validators'; import { useNetworkPageStore } from '../hooks/useNetworkPageStore'; import { DividerHeader } from './components/DividerHeader.tsx'; @@ -119,15 +120,15 @@ export const NetworkEditForm = () => { .string() .trim() .min(1, LL.form.error.required()) - .refine((value) => { - return validateIpList(value, ',', true); + .refine((val) => { + return Validate.any(val, [Validate.CIDRv4, Validate.CIDRv6], true); }, LL.form.error.addressNetmask()), endpoint: z .string() .trim() .min(1, LL.form.error.required()) .refine( - (val) => validateIpOrDomain(val, false, true), + (val) => Validate.any(val, [Validate.IPv4, Validate.IPv6, Validate.Domain]), LL.form.error.endpoint(), ), port: z @@ -135,17 +136,34 @@ export const NetworkEditForm = () => { invalid_type_error: LL.form.error.required(), }) .max(65535, LL.form.error.portMax()), - allowed_ips: z.string(), + allowed_ips: z + .string() + .trim() + .optional() + .refine( + (val) => + Validate.any( + val, + [ + Validate.CIDRv4, + Validate.IPv4, + Validate.CIDRv6, + Validate.IPv6, + Validate.Empty, + ], + true, + ), + LL.form.error.address(), + ), dns: z .string() .trim() .optional() - .refine((val) => { - if (val === '' || !val) { - return true; - } - return validateIpOrDomainList(val, ',', false, true); - }, LL.form.error.allowedIps()), + .refine( + (val) => + Validate.any(val, [Validate.IPv4, Validate.IPv6, Validate.Empty], true), + LL.form.error.address(), + ), allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())), keepalive_interval: z .number({ @@ -161,6 +179,7 @@ export const NetworkEditForm = () => { acl_enabled: z.boolean(), acl_default_allow: z.boolean(), location_mfa_mode: z.nativeEnum(LocationMfaMode), + service_location_mode: z.nativeEnum(ServiceLocationMode), }), [LL.form.error], ); @@ -181,6 +200,7 @@ export const NetworkEditForm = () => { acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode.DISABLED, + service_location_mode: ServiceLocationMode.DISABLED, }), [], ); @@ -237,7 +257,7 @@ export const NetworkEditForm = () => { return defaultValues; }, [defaultValues, networkToForm, networks, selectedNetworkId]); - const { control, handleSubmit, reset } = useForm({ + const { control, handleSubmit, reset, setValue } = useForm({ defaultValues: defaultFormValues, resolver: zodResolver(zodSchema), mode: 'all', @@ -257,6 +277,25 @@ export const NetworkEditForm = () => { () => locationMfaMode === LocationMfaMode.DISABLED, [locationMfaMode], ); + const serviceLocationMode = useWatch({ + control, + name: 'service_location_mode', + defaultValue: defaultFormValues.service_location_mode, + }); + const serviceLocationEnabled = useMemo( + () => serviceLocationMode !== ServiceLocationMode.DISABLED, + [serviceLocationMode], + ); + + useEffect(() => { + if (!mfaDisabled && serviceLocationMode !== ServiceLocationMode.DISABLED) { + setValue('service_location_mode', ServiceLocationMode.DISABLED, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true, + }); + } + }, [mfaDisabled, serviceLocationMode, setValue]); const onValidSubmit: SubmitHandler = (values) => { if (selectedNetworkId) { @@ -387,7 +426,17 @@ export const NetworkEditForm = () => { - + {serviceLocationEnabled && ( + +

+ {LL.networkConfiguration.form.helpers.locationMfaMode.serviceLocationWarning()} +

+
+ )} +

{LL.networkConfiguration.form.helpers.peerDisconnectThreshold()}

@@ -397,6 +446,31 @@ export const NetworkEditForm = () => { type="number" disabled={mfaDisabled} /> + + + +
    +
  • +

    {LL.networkConfiguration.form.helpers.serviceLocation.preLogon()}

    +
  • +
  • +

    {LL.networkConfiguration.form.helpers.serviceLocation.alwaysOn()}

    +
  • +
+
+ {!mfaDisabled && ( + +

{LL.networkConfiguration.form.helpers.serviceLocation.mfaWarning()}

+
+ )} + diff --git a/web/src/pages/network/NetworkEditForm/style.scss b/web/src/pages/network/NetworkEditForm/style.scss index c8840e9d88..b66171214d 100644 --- a/web/src/pages/network/NetworkEditForm/style.scss +++ b/web/src/pages/network/NetworkEditForm/style.scss @@ -43,6 +43,19 @@ } } + #service-location-mode-explain-message-box { + ul { + list-style-position: inside; + margin-top: 8px; + + li { + p { + display: inline; + } + } + } + } + .divider-header { padding-bottom: var(--spacing-s); diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx index d7df932dec..c8bcdc885d 100644 --- a/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx +++ b/web/src/pages/settings/components/EnterpriseSettings/components/EnterpriseForm.tsx @@ -12,6 +12,7 @@ import useApi from '../../../../../shared/hooks/useApi'; import { useToaster } from '../../../../../shared/hooks/useToaster'; import { MutationKeys } from '../../../../../shared/mutations'; import { QueryKeys } from '../../../../../shared/queries'; +import { ClientTrafficPolicySelect } from './TrafficPolicySelect/TrafficPolicySelect'; export const EnterpriseForm = () => { const { LL } = useI18nContext(); @@ -77,17 +78,10 @@ export const EnterpriseForm = () => {
- - mutate({ disable_all_traffic: !settings.disable_all_traffic }) - } + mutate({ client_traffic_policy: value })} + fieldValue={settings.client_traffic_policy} /> - - {parse(LL.settingsPage.enterprise.fields.disableAllTraffic.helper())} -
diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx new file mode 100644 index 0000000000..2faf81784e --- /dev/null +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/TrafficPolicySelect.tsx @@ -0,0 +1,90 @@ +import './style.scss'; +import clsx from 'clsx'; +import { useMemo } from 'react'; +import { useI18nContext } from '../../../../../../i18n/i18n-react'; +import { MessageBox } from '../../../../../../shared/defguard-ui/components/Layout/MessageBox/MessageBox'; +import { RadioButton } from '../../../../../../shared/defguard-ui/components/Layout/RadioButton/Radiobutton'; +import type { SelectOption } from '../../../../../../shared/defguard-ui/components/Layout/Select/types'; +import { ClientTrafficPolicy } from '../../../../../../shared/types'; + +type Props = { + onChange: (event: ClientTrafficPolicy) => void; + fieldValue: ClientTrafficPolicy; +}; + +export const ClientTrafficPolicySelect = ({ onChange, fieldValue }: Props) => { + const { LL } = useI18nContext(); + const options = useMemo( + (): SelectOption[] => [ + { + key: ClientTrafficPolicy.NONE, + value: ClientTrafficPolicy.NONE, + label: LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.label(), + }, + { + key: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, + value: ClientTrafficPolicy.DISABLE_ALL_TRAFFIC, + label: + LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label(), + }, + { + key: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, + value: ClientTrafficPolicy.FORCE_ALL_TRAFFIC, + label: + LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label(), + }, + ], + [ + LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.label, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.label, + LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.label, + ], + ); + + return ( +
+
+

{LL.settingsPage.enterprise.fields.clientTrafficPolicy.header()}

+
+
+ +
    +
  • +

    {LL.settingsPage.enterprise.fields.clientTrafficPolicy.none.helper()}

    +
  • +
  • +

    + {LL.settingsPage.enterprise.fields.clientTrafficPolicy.disableAllTraffic.helper()} +

    +
  • +
  • +

    + {LL.settingsPage.enterprise.fields.clientTrafficPolicy.forceAllTraffic.helper()} +

    +
  • +
+
+ {options.map(({ key, value, label, disabled = false }) => { + const active = fieldValue === value; + return ( +
{ + if (!disabled) { + onChange(value); + } + }} + > +

{label}

+ +
+ ); + })} +
+
+ ); +}; diff --git a/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss new file mode 100644 index 0000000000..ad3f917539 --- /dev/null +++ b/web/src/pages/settings/components/EnterpriseSettings/components/TrafficPolicySelect/style.scss @@ -0,0 +1,72 @@ +.client-traffic-policy-select { + display: flex; + flex-flow: column; + row-gap: var(--spacing-s); + margin-bottom: 25px; + + .client-traffic-policy { + display: flex; + align-items: center; + justify-content: space-between; + column-gap: var(--spacing-xs); + min-height: 30px; + border: 1px solid var(--border-primary); + padding: var(--spacing-xs) var(--spacing-s); + border-radius: 10px; + cursor: pointer; + user-select: none; + transition-property: border-color, opacity; + @include animate-standard; + + &:not(.active) { + &:hover { + border-color: var(--border-separator); + } + } + + &.active { + border-color: var(--surface-main-primary); + } + + &.active, + &:hover { + .label { + color: var(--text-body-primary); + } + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--surface-secondary); + + .label { + color: var(--text-body-disabled); + } + + &:hover { + border-color: var(--border-primary); + } + } + + .label { + color: var(--text-body-secondary); + transition-property: color; + @include typography(app-modal-1); + @include animate-standard; + } + } + + #client-traffic-policy-message-box { + ul { + list-style-position: inside; + margin-top: 8px; + + li { + p { + display: inline; + } + } + } + } +} diff --git a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx index a859fbbfca..d60e136342 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/DirectorySyncSettings.tsx @@ -5,10 +5,10 @@ import { useMemo, useState } from 'react'; import { useFormContext, useWatch } from 'react-hook-form'; import { useI18nContext } from '../../../../../i18n/i18n-react'; +import { FormCheckBox } from '../../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox'; import { FormInput } from '../../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; import { FormSelect } from '../../../../../shared/defguard-ui/components/Form/FormSelect/FormSelect'; import { Helper } from '../../../../../shared/defguard-ui/components/Layout/Helper/Helper'; -import { LabeledCheckbox } from '../../../../../shared/defguard-ui/components/Layout/LabeledCheckbox/LabeledCheckbox'; import SvgIconDownload from '../../../../../shared/defguard-ui/components/svg/IconDownload'; import { titleCase } from '../../../../../shared/utils/titleCase'; import { SUPPORTED_SYNC_PROVIDERS } from './SupportedProviders'; @@ -80,16 +80,11 @@ export const DirsyncSettings = ({ isLoading }: { isLoading: boolean }) => {
{showDirsync ? ( <> -
- {/* FIXME: Really buggy when using the controller, investigate why */} - setValue('directory_sync_enabled', val)} - // controller={{ control, name: 'directory_sync_enabled' }} - /> -
+ { disabled={isLoading} /> {providerName === 'Microsoft' ? ( - {parse(localLL.form.labels.group_match.helper())} - } - required={false} - > + <> +
+ + {localLL.form.labels.prefetch_users.helper()} +
+ {parse(localLL.form.labels.group_match.helper())} + } + required={false} + /> + ) : null} {providerName === 'Okta' ? ( <> diff --git a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx index ec3215c2b4..992c57a543 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx +++ b/web/src/pages/settings/components/OpenIdSettings/components/OpenIdSettingsForm.tsx @@ -102,6 +102,7 @@ export const OpenIdSettingsForm = () => { google_service_account_email: z.string(), google_service_account_key: z.string(), directory_sync_enabled: z.boolean(), + prefetch_users: z.boolean(), directory_sync_interval: z.number().min(60, LL.form.error.invalid()), directory_sync_user_behavior: z.enum(['keep', 'disable', 'delete']), directory_sync_admin_behavior: z.enum(['keep', 'disable', 'delete']), @@ -175,6 +176,7 @@ export const OpenIdSettingsForm = () => { google_service_account_email: '', google_service_account_key: '', directory_sync_enabled: false, + prefetch_users: false, directory_sync_interval: 600, directory_sync_user_behavior: 'keep', directory_sync_admin_behavior: 'keep', diff --git a/web/src/pages/settings/components/OpenIdSettings/components/style.scss b/web/src/pages/settings/components/OpenIdSettings/components/style.scss index 017b6c9d4e..0e9f73a849 100644 --- a/web/src/pages/settings/components/OpenIdSettings/components/style.scss +++ b/web/src/pages/settings/components/OpenIdSettings/components/style.scss @@ -76,8 +76,14 @@ justify-content: flex-end; } - .labeled-checkbox { - padding-bottom: var(--spacing-s); + #directory-sync-settings { + & > .form-checkbox { + padding-bottom: var(--spacing-s); + } + + .helper-row { + padding-bottom: var(--spacing-s); + } } } diff --git a/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx b/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx index 55db9997b5..f28d33c025 100644 --- a/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx +++ b/web/src/pages/settings/components/SmtpSettings/components/SmtpSettingsForm/SmtpSettingsForm.tsx @@ -27,7 +27,7 @@ import { patternValidEmail } from '../../../../../../shared/patterns'; import { QueryKeys } from '../../../../../../shared/queries'; import type { SettingsSMTP } from '../../../../../../shared/types'; import { invalidateMultipleQueries } from '../../../../../../shared/utils/invalidateMultipleQueries'; -import { validateIpOrDomain } from '../../../../../../shared/validators'; +import { Validate } from '../../../../../../shared/validators'; import { useSettingsPage } from '../../../../hooks/useSettingsPage'; import { SmtpTestModal } from '../SmtpTest/SmtpTestModal'; import { useSmtpTestModal } from '../SmtpTest/useSmtpTestModal'; @@ -112,8 +112,14 @@ export const SmtpSettingsForm = () => { .trim() .min(1, LL.form.error.required()) .refine( - (val) => (!val ? true : validateIpOrDomain(val, false, true)), - LL.form.error.endpoint(), + (val) => + Validate.any(val, [ + Validate.IPv4, + Validate.IPv6, + Validate.Domain, + Validate.Empty, + ]), + LL.form.error.address(), ), smtp_port: z .number({ diff --git a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx index caa6ef8530..993154b893 100644 --- a/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx +++ b/web/src/pages/wizard/components/WizardNetworkConfiguration/WizardNetworkConfiguration.tsx @@ -10,6 +10,7 @@ import { shallow } from 'zustand/shallow'; import { useI18nContext } from '../../../../i18n/i18n-react'; import { FormAclDefaultPolicy } from '../../../../shared/components/Form/FormAclDefaultPolicySelect/FormAclDefaultPolicy.tsx'; import { FormLocationMfaModeSelect } from '../../../../shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx'; +import { FormServiceLocationModeSelect } from '../../../../shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect.tsx'; import { RenderMarkdown } from '../../../../shared/components/Layout/RenderMarkdown/RenderMarkdown.tsx'; import { FormCheckBox } from '../../../../shared/defguard-ui/components/Form/FormCheckBox/FormCheckBox.tsx'; import { FormInput } from '../../../../shared/defguard-ui/components/Form/FormInput/FormInput'; @@ -22,10 +23,10 @@ import { useAppStore } from '../../../../shared/hooks/store/useAppStore.ts'; import useApi from '../../../../shared/hooks/useApi'; import { useToaster } from '../../../../shared/hooks/useToaster'; import { QueryKeys } from '../../../../shared/queries'; -import { LocationMfaMode } from '../../../../shared/types.ts'; +import { LocationMfaMode, ServiceLocationMode } from '../../../../shared/types.ts'; import { titleCase } from '../../../../shared/utils/titleCase'; import { trimObjectStrings } from '../../../../shared/utils/trimObjectStrings.ts'; -import { validateIpList, validateIpOrDomainList } from '../../../../shared/validators'; +import { Validate } from '../../../../shared/validators'; import { useWizardStore } from '../../hooks/useWizardStore'; import { DividerHeader } from './components/DividerHeader.tsx'; @@ -106,27 +107,52 @@ export const WizardNetworkConfiguration = () => { .string() .trim() .min(1, LL.form.error.required()) - .refine((value) => { - return validateIpList(value, ',', true); - }, LL.form.error.addressNetmask()), - endpoint: z.string().trim().min(1, LL.form.error.required()), + .refine( + (val) => Validate.any(val, [Validate.CIDRv4, Validate.CIDRv6]), + LL.form.error.addressNetmask(), + ), + endpoint: z + .string() + .trim() + .min(1, LL.form.error.required()) + .refine( + (val) => + Validate.any(val, [Validate.IPv4, Validate.IPv6, Validate.Domain], true), + LL.form.error.endpoint(), + ), port: z .number({ invalid_type_error: LL.form.error.invalid(), }) .max(65535, LL.form.error.portMax()) .nonnegative(), - allowed_ips: z.string().trim(), + allowed_ips: z + .string() + .trim() + .refine( + (val) => + Validate.any( + val, + [ + Validate.CIDRv4, + Validate.IPv4, + Validate.CIDRv6, + Validate.IPv6, + Validate.Empty, + ], + true, + ), + LL.form.error.address(), + ), dns: z .string() .trim() .optional() - .refine((val) => { - if (val === '' || !val) { - return true; - } - return validateIpOrDomainList(val, ',', true); - }, LL.form.error.allowedIps()), + .refine( + (val) => + Validate.any(val, [Validate.IPv4, Validate.IPv6, Validate.Empty], true), + LL.form.error.address(), + ), allowed_groups: z.array(z.string().trim().min(1, LL.form.error.minimumLength())), keepalive_interval: z .number({ @@ -141,6 +167,7 @@ export const WizardNetworkConfiguration = () => { acl_enabled: z.boolean(), acl_default_allow: z.boolean(), location_mfa_mode: z.nativeEnum(LocationMfaMode), + service_location_mode: z.nativeEnum(ServiceLocationMode), }), [LL.form.error], ); @@ -171,6 +198,15 @@ export const WizardNetworkConfiguration = () => { () => locationMfaMode === LocationMfaMode.DISABLED, [locationMfaMode], ); + const serviceLocationMode = useWatch({ + control, + name: 'service_location_mode', + defaultValue: getDefaultValues.service_location_mode, + }); + const serviceLocationEnabled = useMemo( + () => serviceLocationMode !== ServiceLocationMode.DISABLED, + [serviceLocationMode], + ); const handleValidSubmit: SubmitHandler = (values) => { const trimmed = trimObjectStrings(values); @@ -282,7 +318,17 @@ export const WizardNetworkConfiguration = () => { - + {serviceLocationEnabled && ( + +

+ {LL.networkConfiguration.form.helpers.locationMfaMode.serviceLocationWarning()} +

+
+ )} +

{LL.networkConfiguration.form.helpers.peerDisconnectThreshold()}

@@ -292,6 +338,15 @@ export const WizardNetworkConfiguration = () => { type="number" disabled={mfaDisabled} /> + {!mfaDisabled && ( + +

{LL.networkConfiguration.form.helpers.serviceLocation.mfaWarning()}

+
+ )} + diff --git a/web/src/pages/wizard/components/WizardNetworkImport/WizardNetworkImport.tsx b/web/src/pages/wizard/components/WizardNetworkImport/WizardNetworkImport.tsx index 52c798c0ee..7f38c6d255 100644 --- a/web/src/pages/wizard/components/WizardNetworkImport/WizardNetworkImport.tsx +++ b/web/src/pages/wizard/components/WizardNetworkImport/WizardNetworkImport.tsx @@ -27,7 +27,7 @@ import { QueryKeys } from '../../../../shared/queries'; import type { ImportNetworkRequest } from '../../../../shared/types'; import { invalidateMultipleQueries } from '../../../../shared/utils/invalidateMultipleQueries'; import { titleCase } from '../../../../shared/utils/titleCase'; -import { validateIpOrDomain } from '../../../../shared/validators'; +import { Validate } from '../../../../shared/validators'; import { useWizardStore } from '../../hooks/useWizardStore'; interface FormInputs extends Omit { @@ -70,7 +70,10 @@ export const WizardNetworkImport = () => { .string() .trim() .min(1, LL.form.error.required()) - .refine((val) => validateIpOrDomain(val), LL.form.error.endpoint()), + .refine( + (val) => Validate.any(val, [Validate.IPv4, Validate.IPv6, Validate.Domain]), + LL.form.error.endpoint(), + ), fileName: z.string().trim().min(1, LL.form.error.required()), config: z.string().trim().min(1, LL.form.error.required()), allowed_groups: z.array(z.string().min(1, LL.form.error.minimumLength())), diff --git a/web/src/pages/wizard/hooks/useWizardStore.ts b/web/src/pages/wizard/hooks/useWizardStore.ts index 0206902238..944f9b48f0 100644 --- a/web/src/pages/wizard/hooks/useWizardStore.ts +++ b/web/src/pages/wizard/hooks/useWizardStore.ts @@ -7,6 +7,7 @@ import { type ImportedDevice, LocationMfaMode, type Network, + ServiceLocationMode, } from '../../../shared/types'; export enum WizardSetupType { @@ -34,6 +35,7 @@ const defaultValues: StoreFields = { acl_enabled: false, acl_default_allow: false, location_mfa_mode: LocationMfaMode.DISABLED, + service_location_mode: ServiceLocationMode.DISABLED, }, }; @@ -90,6 +92,7 @@ type StoreFields = { acl_enabled: boolean; acl_default_allow: boolean; location_mfa_mode: LocationMfaMode; + service_location_mode: ServiceLocationMode; }; }; diff --git a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx index 0c87aae1c7..23e89e11c0 100644 --- a/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx +++ b/web/src/shared/components/Form/FormLocationMfaModeSelect/FormLocationMfaModeSelect.tsx @@ -14,10 +14,12 @@ import { LocationMfaMode } from '../../../types'; type Props = { controller: UseControllerProps; + disabled?: boolean; }; export const FormLocationMfaModeSelect = ({ controller, + disabled = false, }: Props) => { const { LL } = useI18nContext(); const { @@ -38,12 +40,13 @@ export const FormLocationMfaModeSelect = ({ key: LocationMfaMode.INTERNAL, value: LocationMfaMode.INTERNAL, label: LL.components.locationMfaModeSelect.options.internal(), + disabled: disabled, }, { key: LocationMfaMode.EXTERNAL, value: LocationMfaMode.EXTERNAL, label: LL.components.locationMfaModeSelect.options.external(), - disabled: externalMfaDisabled, + disabled: externalMfaDisabled || disabled, }, ], [ @@ -51,6 +54,7 @@ export const FormLocationMfaModeSelect = ({ LL.components.locationMfaModeSelect.options.external, LL.components.locationMfaModeSelect.options.internal, externalMfaDisabled, + disabled, ], ); diff --git a/web/src/shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect.tsx b/web/src/shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect.tsx new file mode 100644 index 0000000000..150c158f1d --- /dev/null +++ b/web/src/shared/components/Form/FormServiceLocationModeSelect/FormServiceLocationModeSelect.tsx @@ -0,0 +1,84 @@ +import './style.scss'; +import clsx from 'clsx'; +import { useMemo } from 'react'; +import { + type FieldValues, + type UseControllerProps, + useController, +} from 'react-hook-form'; +import { useI18nContext } from '../../../../i18n/i18n-react'; +import { RadioButton } from '../../../defguard-ui/components/Layout/RadioButton/Radiobutton'; +import type { SelectOption } from '../../../defguard-ui/components/Layout/Select/types'; +import { useAppStore } from '../../../hooks/store/useAppStore'; +import { ServiceLocationMode } from '../../../types'; + +type Props = { + controller: UseControllerProps; + disabled?: boolean; +}; + +export const FormServiceLocationModeSelect = ({ + controller, + disabled = false, +}: Props) => { + const { LL } = useI18nContext(); + const { + field: { onChange, value: fieldValue }, + } = useController(controller); + const enterpriseEnabled = useAppStore((s) => s.appInfo?.license_info.enterprise); + + const options = useMemo( + (): SelectOption[] => [ + { + key: ServiceLocationMode.DISABLED, + value: ServiceLocationMode.DISABLED, + label: LL.components.serviceLocationModeSelect.options.disabled(), + }, + { + key: ServiceLocationMode.PRELOGON, + value: ServiceLocationMode.PRELOGON, + label: LL.components.serviceLocationModeSelect.options.prelogon(), + disabled: !enterpriseEnabled || disabled, + }, + { + key: ServiceLocationMode.ALWAYSON, + value: ServiceLocationMode.ALWAYSON, + label: LL.components.serviceLocationModeSelect.options.alwayson(), + disabled: !enterpriseEnabled || disabled, + }, + ], + [ + LL.components.serviceLocationModeSelect.options.disabled, + LL.components.serviceLocationModeSelect.options.prelogon, + LL.components.serviceLocationModeSelect.options.alwayson, + disabled, + enterpriseEnabled, + ], + ); + + return ( +
+ + {options.map(({ key, value, label, disabled = false }) => { + const active = fieldValue === value; + return ( +
{ + if (!disabled) { + onChange(value); + } + }} + > +

{label}

+ +
+ ); + })} +
+ ); +}; diff --git a/web/src/shared/components/Form/FormServiceLocationModeSelect/style.scss b/web/src/shared/components/Form/FormServiceLocationModeSelect/style.scss new file mode 100644 index 0000000000..4cc39d601e --- /dev/null +++ b/web/src/shared/components/Form/FormServiceLocationModeSelect/style.scss @@ -0,0 +1,59 @@ +.service-location-mode-select { + display: flex; + flex-flow: column; + row-gap: var(--spacing-s); + margin-bottom: 25px; + + .service-location-mode { + display: flex; + align-items: center; + justify-content: space-between; + column-gap: var(--spacing-xs); + min-height: 30px; + border: 1px solid var(--border-primary); + padding: var(--spacing-xs) var(--spacing-s); + border-radius: 10px; + cursor: pointer; + user-select: none; + transition-property: border-color, opacity; + @include animate-standard; + + &:not(.active) { + &:hover { + border-color: var(--border-separator); + } + } + + &.active { + border-color: var(--surface-main-primary); + } + + &.active, + &:hover { + .label { + color: var(--text-body-primary); + } + } + + &.disabled { + opacity: 0.6; + cursor: not-allowed; + background-color: var(--surface-secondary); + + .label { + color: var(--text-body-disabled); + } + + &:hover { + border-color: var(--border-primary); + } + } + + .label { + color: var(--text-body-secondary); + transition-property: color; + @include typography(app-modal-1); + @include animate-standard; + } + } +} diff --git a/web/src/shared/patterns.ts b/web/src/shared/patterns.ts index cfb5db0bdb..b9c4d6cb7b 100644 --- a/web/src/shared/patterns.ts +++ b/web/src/shared/patterns.ts @@ -63,9 +63,14 @@ export const patternValidUrl = new RegExp( '$', 'i', ); - -export const patternValidDomain = - /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))(?::[0-9]{1,5})?$/; +export const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/; +export const ipv4WithPortPattern = /^(\d{1,3}\.){3}\d{1,3}:\d{1,5}$/; +export const ipv4WithCIDRPattern = /^(\d{1,3}\.){3}\d{1,3}\/([0-9]|[1-2][0-9]|3[0-2])$/; +export const domainPattern = + /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3}))$/; + +export const domainWithPortPattern = + /^(?:(?:(?:[A-Za-z-]+):\/{1,3})?(?:[A-Za-z0-9])(?:[A-Za-z0-9\-.]){1,61}(?:\.[A-Za-z]{2,})+|\[(?:(?:(?:[a-fA-F0-9]){1,4})(?::(?:[a-fA-F0-9]){1,4}){7}|::1|::)\]|(?:(?:[0-9]{1,3})(?:\.[0-9]{1,3}){3})):[0-9]{1,5}$/; export const patternSafeUsernameCharacters = /^[a-zA-Z0-9]+[a-zA-Z0-9.\-_]*$/; diff --git a/web/src/shared/types.ts b/web/src/shared/types.ts index d423ceed58..86bf0baca2 100644 --- a/web/src/shared/types.ts +++ b/web/src/shared/types.ts @@ -139,6 +139,12 @@ export enum LocationMfaMode { EXTERNAL = 'external', } +export enum ServiceLocationMode { + DISABLED = 'disabled', + PRELOGON = 'prelogon', + ALWAYSON = 'alwayson', +} + export interface Network { id: number; name: string; @@ -156,6 +162,7 @@ export interface Network { acl_enabled: boolean; acl_default_allow: boolean; location_mfa_mode: LocationMfaMode; + service_location_mode: ServiceLocationMode; } export type ModifyNetworkRequest = { @@ -164,7 +171,7 @@ export type ModifyNetworkRequest = { Network, 'gateways' | 'connected' | 'id' | 'connected_at' | 'allowed_ips' > & { - allowed_ips: string; + allowed_ips?: string; }; }; @@ -1114,9 +1121,15 @@ export type SettingsGatewayNotifications = { gateway_disconnect_notifications_reconnect_notification_enabled: boolean; }; +export enum ClientTrafficPolicy { + NONE = 'none', + DISABLE_ALL_TRAFFIC = 'disable_all_traffic', + FORCE_ALL_TRAFFIC = 'force_all_traffic', +} + export type SettingsEnterprise = { admin_device_management: boolean; - disable_all_traffic: boolean; + client_traffic_policy: ClientTrafficPolicy; only_client_activation: boolean; }; diff --git a/web/src/shared/validators.ts b/web/src/shared/validators.ts index 4bc73aa9df..23b24bbfc0 100644 --- a/web/src/shared/validators.ts +++ b/web/src/shared/validators.ts @@ -1,7 +1,12 @@ import ipaddr from 'ipaddr.js'; import { z } from 'zod'; - -import { patternValidDomain, patternValidWireguardKey } from './patterns'; +import { + domainPattern, + ipv4Pattern, + ipv4WithCIDRPattern, + ipv4WithPortPattern, + patternValidWireguardKey, +} from './patterns'; export const validateWireguardPublicKey = (props: { requiredError: string; @@ -18,79 +23,155 @@ export const validateWireguardPublicKey = (props: { .max(44, props.maxError) .regex(patternValidWireguardKey, props.validKeyError); -// Returns false when invalid -export const validateIpOrDomain = ( - val: string, - allowMask = false, - allowIPv6 = false, -): boolean => { - return ( - (allowIPv6 && validateIPv6(val, allowMask)) || - validateIPv4(val, allowMask) || - patternValidDomain.test(val) - ); -}; +export const Validate = { + IPv4: (ip: string): boolean => { + if (!ipv4Pattern.test(ip)) { + return false; + } + if (!ipaddr.IPv4.isValid(ip)) { + return false; + } + return true; + }, + IPv4withPort: (ip: string): boolean => { + if (!ipv4WithPortPattern.test(ip)) { + return false; + } + const addr = ip.split(':'); + if (!ipaddr.IPv4.isValid(addr[0]) || !Validate.Port(addr[1])) { + return false; + } + return true; + }, + IPv6: (ip: string): boolean => { + if (!ipaddr.IPv6.isValid(ip)) { + return false; + } + return true; + }, + IPv6withPort: (ip: string): boolean => { + if (ip.includes(']')) { + const address = ip.split(']'); + const ipv6 = address[0].replaceAll('[', '').replaceAll(']', ''); + const port = address[1].replaceAll(']', '').replaceAll(':', ''); + if (!ipaddr.IPv6.isValid(ipv6)) { + return false; + } + if (!Validate.Port(port)) { + return false; + } + } else { + return false; + } + return true; + }, + CIDRv4: (ip: string): boolean => { + if (!ipv4WithCIDRPattern.test(ip)) { + return false; + } + if (ip.endsWith('/0')) { + return false; + } + if (!ipaddr.IPv4.isValidCIDR(ip)) { + return false; + } + return true; + }, + CIDRv6: (ip: string): boolean => { + if (ip.endsWith('/0')) { + return false; + } + if (!ipaddr.IPv6.isValidCIDR(ip)) { + return false; + } + return true; + }, + Domain: (ip: string): boolean => { + if (!domainPattern.test(ip)) { + return false; + } + return true; + }, + DomainWithPort: (ip: string): boolean => { + const splitted = ip.split(':'); + const domain = splitted[0]; + const port = splitted[1]; + if (!Validate.Port(port)) { + return false; + } + if (!domainPattern.test(domain)) { + return false; + } + return true; + }, + Port: (val: string): boolean => { + const parsed = Number(val); + if (Number.isNaN(parsed) || !Number.isInteger(parsed)) { + return false; + } + return 0 < parsed && parsed <= 65535; + }, + Empty: (val: string): boolean => { + if (val === '' || !val) { + return true; + } + return false; + }, + any: ( + value: string | undefined, + validators: Array<(val: string) => boolean>, + allowList: boolean = false, + splitWith = ',', + ): boolean => { + if (!value) { + return true; + } + const items = value.replaceAll(' ', '').split(splitWith); -// Returns false when invalid -export const validateIpList = ( - val: string, - splitWith = ',', - allowMasks = false, -): boolean => { - return val - .replace(' ', '') - .split(splitWith) - .every((el) => { - return validateIPv4(el, allowMasks) || validateIPv6(el, allowMasks); - }); -}; + if (items.length > 1 && !allowList) { + return false; + } -// Returns false when invalid -export const validateIpOrDomainList = ( - val: string, - splitWith = ',', - allowMasks = false, - allowIPv6 = false, -): boolean => { - const trimmed = val.replace(' ', ''); - const split = trimmed.split(splitWith); - for (const value of split) { - if ( - !validateIPv4(value, allowMasks) && - !patternValidDomain.test(value) && - (!allowIPv6 || !validateIPv6(value, allowMasks)) - ) { - return false; - } - } - return true; -}; + for (const item of items) { + let valid = false; + for (const validator of validators) { + if (validator(item)) { + valid = true; + break; + } + } + if (!valid) { + return false; + } + } -// Returns false when invalid -export const validateIPv4 = (ip: string, allowMask = false): boolean => { - if (allowMask) { - if (ip.includes('/')) { - return ipaddr.IPv4.isValidCIDR(ip); - } - } - return ipaddr.IPv4.isValid(ip); -}; + return true; + }, + all: ( + value: string | undefined, + validators: Array<(val: string) => boolean>, + allowList: boolean = false, + splitWith = ',', + ): boolean => { + if (!value) { + return true; + } + const items = value.replaceAll(' ', '').split(splitWith); -export const validateIPv6 = (ip: string, allowMask = false): boolean => { - if (allowMask) { - if (ip.includes('/')) { - return ipaddr.IPv6.isValidCIDR(ip); - } - } - return ipaddr.IPv6.isValid(ip); -}; + if (items.length > 1 && !allowList) { + return false; + } + for (const item of items) { + for (const validator of validators) { + if (!validator(item)) { + return false; + } + } + } -export const validatePort = (val: string) => { - const parsed = parseInt(val, 10); - if (!Number.isNaN(parsed)) { - return parsed <= 65535; - } -}; + return true; + }, +} as const; export const numericString = (val: string) => /^\d+$/.test(val); diff --git a/web/tests/validators.test.ts b/web/tests/validators.test.ts new file mode 100644 index 0000000000..08f8000998 --- /dev/null +++ b/web/tests/validators.test.ts @@ -0,0 +1,301 @@ +import { describe, expect, it } from 'vitest'; +import { Validate } from '../src/shared/validators'; + +describe('Validate.IPv4', () => { + it('should accept valid IPv4 addresses', () => { + expect(Validate.IPv4('192.168.1.1')).toBe(true); + expect(Validate.IPv4('10.0.0.1')).toBe(true); + expect(Validate.IPv4('172.16.0.1')).toBe(true); + expect(Validate.IPv4('255.255.255.255')).toBe(true); + expect(Validate.IPv4('0.0.0.0')).toBe(true); + }); + + it('should reject invalid IPv4 addresses', () => { + expect(Validate.IPv4('1')).toBe(false); + expect(Validate.IPv4('256.1.1.1')).toBe(false); + expect(Validate.IPv4('192.168.1')).toBe(false); + expect(Validate.IPv4('192.168.1.1.1')).toBe(false); + expect(Validate.IPv4('abc.def.ghi.jkl')).toBe(false); + expect(Validate.IPv4('192.168.1.1/24')).toBe(false); + }); + + it('should reject empty strings', () => { + expect(Validate.IPv4('')).toBe(false); + }); +}); + +describe('Validate.IPv4withPort', () => { + it('should accept valid IPv4 with port', () => { + expect(Validate.IPv4withPort('192.168.1.1:8080')).toBe(true); + expect(Validate.IPv4withPort('10.0.0.1:80')).toBe(true); + expect(Validate.IPv4withPort('127.0.0.1:5051')).toBe(true); + expect(Validate.IPv4withPort('192.168.1.1:65535')).toBe(true); + }); + + it('should reject IPv4 without port', () => { + expect(Validate.IPv4withPort('192.168.1.1')).toBe(false); + }); + + it('should reject invalid port numbers', () => { + expect(Validate.IPv4withPort('192.168.1.1:0')).toBe(false); + expect(Validate.IPv4withPort('192.168.1.1:65536')).toBe(false); + expect(Validate.IPv4withPort('192.168.1.1:99999')).toBe(false); + }); + + it('should reject invalid IPv4 format', () => { + expect(Validate.IPv4withPort('256.1.1.1:8080')).toBe(false); + expect(Validate.IPv4withPort('192.168.1:8080')).toBe(false); + }); +}); + +describe('Validate.IPv6', () => { + it('should accept valid IPv6 addresses', () => { + expect(Validate.IPv6('2001:db8::1')).toBe(true); + expect(Validate.IPv6('::1')).toBe(true); + expect(Validate.IPv6('::')).toBe(true); + expect(Validate.IPv6('2001:0db8:0000:0000:0000:0000:0000:0001')).toBe(true); + expect(Validate.IPv6('fe80::1')).toBe(true); + }); + + it('should reject invalid IPv6 addresses', () => { + expect(Validate.IPv6('192.168.1.1')).toBe(false); + expect(Validate.IPv6('gggg::1')).toBe(false); + expect(Validate.IPv6('invalid')).toBe(false); + }); +}); + +describe('Validate.IPv6withPort', () => { + it('should accept valid IPv6 with port in brackets', () => { + expect(Validate.IPv6withPort('[::1]:8080')).toBe(true); + expect(Validate.IPv6withPort('[2001:db8::1]:80')).toBe(true); + expect(Validate.IPv6withPort('[fe80::1]:65535')).toBe(true); + }); + + it('should reject IPv6 without brackets', () => { + expect(Validate.IPv6withPort('::1:8080')).toBe(false); + expect(Validate.IPv6withPort('2001:db8::1:8080')).toBe(false); + }); + + it('should reject IPv6 without port', () => { + expect(Validate.IPv6withPort('[::1]')).toBe(false); + }); + + it('should reject invalid port numbers', () => { + expect(Validate.IPv6withPort('[::1]:0')).toBe(false); + expect(Validate.IPv6withPort('[::1]:65536')).toBe(false); + }); +}); + +describe('Validate.CIDRv4', () => { + it('should accept valid IPv4 CIDR notation', () => { + expect(Validate.CIDRv4('192.168.1.0/24')).toBe(true); + expect(Validate.CIDRv4('10.0.0.0/8')).toBe(true); + expect(Validate.CIDRv4('172.16.0.0/12')).toBe(true); + expect(Validate.CIDRv4('192.168.1.1/32')).toBe(true); + expect(Validate.CIDRv4('192.168.1.0/1')).toBe(true); + }); + + it('should reject CIDR with /0 mask', () => { + expect(Validate.CIDRv4('192.168.1.0/0')).toBe(false); + }); + + it('should reject invalid CIDR masks', () => { + expect(Validate.CIDRv4('192.168.1.0/33')).toBe(false); + expect(Validate.CIDRv4('192.168.1.0/99')).toBe(false); + }); + + it('should reject IPv4 without CIDR mask', () => { + expect(Validate.CIDRv4('192.168.1.1')).toBe(false); + }); + + it('should reject invalid IPv4 in CIDR', () => { + expect(Validate.CIDRv4('256.1.1.1/24')).toBe(false); + expect(Validate.CIDRv4('192.168.1/24')).toBe(false); + }); +}); + +describe('Validate.CIDRv6', () => { + it('should accept valid IPv6 CIDR notation', () => { + expect(Validate.CIDRv6('2001:db8::/32')).toBe(true); + expect(Validate.CIDRv6('fe80::/10')).toBe(true); + expect(Validate.CIDRv6('::1/128')).toBe(true); + }); + + it('should reject CIDR with /0 mask', () => { + expect(Validate.CIDRv6('2001:db8::/0')).toBe(false); + }); + + it('should reject invalid CIDR masks', () => { + expect(Validate.CIDRv6('2001:db8::/129')).toBe(false); + }); + + it('should reject IPv6 without CIDR mask', () => { + expect(Validate.CIDRv6('2001:db8::1')).toBe(false); + }); +}); + +describe('Validate.Domain', () => { + it('should accept valid domain names', () => { + expect(Validate.Domain('example.com')).toBe(true); + expect(Validate.Domain('sub.example.com')).toBe(true); + expect(Validate.Domain('my-domain.co.uk')).toBe(true); + expect(Validate.Domain('test123.example.org')).toBe(true); + }); + + it('should reject domains with port', () => { + expect(Validate.Domain('example.com:8080')).toBe(false); + }); + + it('should reject invalid domain formats', () => { + expect(Validate.Domain('invalid domain')).toBe(false); + expect(Validate.Domain('example..com')).toBe(false); + expect(Validate.Domain('domain.secret.com')).toBe(true); + }); +}); + +describe('Validate.DomainWithPort', () => { + it('should accept valid domains with port', () => { + expect(Validate.DomainWithPort('example.com:8080')).toBe(true); + expect(Validate.DomainWithPort('sub.example.com:443')).toBe(true); + expect(Validate.DomainWithPort('test.org:3000')).toBe(true); + }); + + it('should reject domains without port', () => { + expect(Validate.DomainWithPort('example.com')).toBe(false); + }); + + it('should reject invalid port numbers', () => { + expect(Validate.DomainWithPort('example.com:0')).toBe(false); + expect(Validate.DomainWithPort('example.com:65536')).toBe(false); + expect(Validate.DomainWithPort('example.com:99999')).toBe(false); + }); +}); + +describe('Validate.Port', () => { + it('should accept valid port numbers', () => { + expect(Validate.Port('1')).toBe(true); + expect(Validate.Port('80')).toBe(true); + expect(Validate.Port('443')).toBe(true); + expect(Validate.Port('8080')).toBe(true); + expect(Validate.Port('65535')).toBe(true); + }); + + it('should reject port 0', () => { + expect(Validate.Port('0')).toBe(false); + }); + + it('should reject ports above 65535', () => { + expect(Validate.Port('65536')).toBe(false); + expect(Validate.Port('99999')).toBe(false); + }); + + it('should reject non-numeric values', () => { + expect(Validate.Port('abc')).toBe(false); + expect(Validate.Port('12.34')).toBe(false); + expect(Validate.Port('')).toBe(false); + }); + + it('should reject negative numbers', () => { + expect(Validate.Port('-1')).toBe(false); + }); +}); + +describe('Validate.any', () => { + it('should accept single valid value matching any validator', () => { + expect(Validate.any('192.168.1.1', [Validate.IPv4, Validate.IPv6])).toBe(true); + expect(Validate.any('2001:db8::1', [Validate.IPv4, Validate.IPv6])).toBe(true); + expect(Validate.any('example.com', [Validate.Domain, Validate.IPv4])).toBe(true); + }); + + it('should reject single value not matching any validator', () => { + expect(Validate.any('invalid', [Validate.IPv4, Validate.IPv6])).toBe(false); + expect(Validate.any('256.1.1.1', [Validate.IPv4, Validate.IPv6])).toBe(false); + }); + + it('should reject multiple values when allowList is false (default)', () => { + expect(Validate.any('192.168.1.1,10.0.0.1', [Validate.IPv4])).toBe(false); + expect(Validate.any('example.com,test.com', [Validate.Domain])).toBe(false); + }); + + it('should accept multiple valid values when allowList is true', () => { + expect(Validate.any('192.168.1.1,10.0.0.1', [Validate.IPv4], true)).toBe(true); + expect( + Validate.any('192.168.1.1,2001:db8::1', [Validate.IPv4, Validate.IPv6], true), + ).toBe(true); + expect(Validate.any('example.com,test.org', [Validate.Domain], true)).toBe(true); + }); + + it('should reject list with any invalid value when allowList is true', () => { + expect(Validate.any('192.168.1.1,invalid', [Validate.IPv4], true)).toBe(false); + expect(Validate.any('192.168.1.1,256.1.1.1', [Validate.IPv4], true)).toBe(false); + }); + + it('should handle mixed valid values with allowList', () => { + expect( + Validate.any( + '192.168.1.1,2001:db8::1,10.0.0.1', + [Validate.IPv4, Validate.IPv6], + true, + ), + ).toBe(true); + expect( + Validate.any('example.com,192.168.1.1', [Validate.Domain, Validate.IPv4], true), + ).toBe(true); + }); + + it('should handle custom split character', () => { + expect(Validate.any('192.168.1.1;10.0.0.1', [Validate.IPv4], true, ';')).toBe(true); + expect(Validate.any('192.168.1.1|10.0.0.1', [Validate.IPv4], true, '|')).toBe(true); + }); + + it('should handle whitespace in list', () => { + expect(Validate.any('192.168.1.1, 10.0.0.1', [Validate.IPv4], true)).toBe(true); + expect(Validate.any('192.168.1.1 , 10.0.0.1', [Validate.IPv4], true)).toBe(true); + }); + + it('should accept empty string with Empty validator in list', () => { + expect(Validate.any('', [Validate.IPv4, Validate.Empty], true)).toBe(true); + }); +}); + +describe('Validate.all', () => { + it('should accept single value matching all validators', () => { + expect(Validate.all('192.168.1.1', [Validate.IPv4])).toBe(true); + }); + + it('should reject single value not matching all validators', () => { + expect(Validate.all('192.168.1.1', [Validate.IPv4, Validate.IPv6])).toBe(false); + expect(Validate.all('invalid', [Validate.IPv4])).toBe(false); + }); + + it('should accept empty string or undefined', () => { + expect(Validate.all('', [Validate.IPv4])).toBe(true); + expect(Validate.all(undefined, [Validate.IPv4])).toBe(true); + }); + + it('should reject multiple values when allowList is false (default)', () => { + expect(Validate.all('192.168.1.1,10.0.0.1', [Validate.IPv4])).toBe(false); + }); + + it('should accept multiple valid values when allowList is true', () => { + expect(Validate.all('192.168.1.1,10.0.0.1', [Validate.IPv4], true)).toBe(true); + expect(Validate.all('example.com,test.org', [Validate.Domain], true)).toBe(true); + }); + + it('should reject if any value does not match all validators when allowList is true', () => { + expect(Validate.all('192.168.1.1,invalid', [Validate.IPv4], true)).toBe(false); + expect(Validate.all('192.168.1.1,256.1.1.1', [Validate.IPv4], true)).toBe(false); + }); + + it('should handle custom split character', () => { + expect(Validate.all('192.168.1.1;10.0.0.1', [Validate.IPv4], true, ';')).toBe(true); + expect(Validate.all('192.168.1.1|10.0.0.1', [Validate.IPv4], true, '|')).toBe(true); + }); + + it('should handle whitespace in list', () => { + expect(Validate.all('192.168.1.1, 10.0.0.1', [Validate.IPv4], true)).toBe(true); + expect( + Validate.all('192.168.1.1 , 10.0.0.1 , 172.16.0.1', [Validate.IPv4], true), + ).toBe(true); + }); +}); diff --git a/web/vitest.config.mts b/web/vitest.config.mts new file mode 100644 index 0000000000..06e004bc54 --- /dev/null +++ b/web/vitest.config.mts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; +import * as path from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + }, + resolve: { + alias: { + '@scss': path.resolve(__dirname, './src/shared/scss'), + '@scssutils': path.resolve(__dirname, './src/shared/scss/global'), + }, + }, +});