diff --git a/.eslintrc.json b/.eslintrc.json index c606b8f933b..3fd6dec3d7e 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,6 +56,7 @@ "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": ["error", { "checksVoidReturn": false }], "@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["self"] }], + "@typescript-eslint/no-unused-expressions": ["error", { "allowTernary": true }], "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "no-console": "error", "import/no-unresolved": "off", // TODO: Look into turning off once each package is an actual package. @@ -141,8 +142,7 @@ } ] } - ], - "no-restricted-imports": ["error", { "patterns": ["src/**/*"] }] + ] } }, { @@ -164,147 +164,6 @@ "tailwindcss/no-contradicting-classname": "error" } }, - { - "files": ["libs/admin-console/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/admin-console/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/angular/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/angular/*", "src/**/*"] }] - } - }, - { - "files": ["libs/auth/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/auth/*", "src/**/*"] }] - } - }, - { - "files": ["libs/billing/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/billing/*", "src/**/*"] }] - } - }, - { - "files": ["libs/common/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/common/*", "src/**/*"] }] - } - }, - { - "files": ["libs/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/components/*", "src/**/*", "@bitwarden/angular/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/components/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-components/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/history/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-history/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/legacy/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-legacy/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/generator/extensions/navigation/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/generator-navigation/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-core/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-core/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": [ - "error", - { "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] } - ] - } - }, - { - "files": ["libs/importer/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/importer/*", "src/**/*"] }] - } - }, - { - "files": ["libs/node/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/node/*", "src/**/*"] }] - } - }, - { - "files": ["libs/platform/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/platform/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/send/send-ui/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/send-ui/*", "src/**/*"] }] - } - }, - { - "files": ["libs/tools/card/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/tools-card/*", "src/**/*"] }] - } - }, - { - "files": ["libs/vault/src/**/*.ts"], - "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/vault/*", "src/**/*"] }] - } - }, { "files": ["apps/browser/src/**/*.ts", "libs/**/*.ts"], "excludedFiles": [ @@ -344,10 +203,30 @@ ] } }, + { + "files": ["**/src/**/*.ts"], + "excludedFiles": ["**/platform/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "**/platform/**/internal", // General internal pattern + // All features that have been converted to barrel files + "**/platform/messaging/**", + "**/src/**/*" // Prevent relative imports across libs. + ] + } + ] + } + }, { "files": ["bitwarden_license/bit-common/src/**/*.ts"], "rules": { - "no-restricted-imports": ["error", { "patterns": ["@bitwarden/bit-common/*", "src/**/*"] }] + "no-restricted-imports": [ + "error", + { "patterns": ["@bitwarden/bit-common/*", "**/src/**/*"] } + ] } }, { @@ -357,7 +236,12 @@ "no-restricted-imports": [ "error", { - "patterns": ["biwarden_license/**", "@bitwarden/bit-common/*", "@bitwarden/bit-web/*"] + "patterns": [ + "biwarden_license/**", + "@bitwarden/bit-common/*", + "@bitwarden/bit-web/*", + "**/src/**/*" + ] } ], // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e38277877bb..cb36d87b9e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -126,6 +126,7 @@ apps/web/src/app/key-management @bitwarden/team-key-management-dev apps/browser/src/key-management @bitwarden/team-key-management-dev apps/cli/src/key-management @bitwarden/team-key-management-dev libs/key-management @bitwarden/team-key-management-dev +libs/common/src/key-management @bitwarden/team-key-management-dev apps/desktop/destkop_native/core/src/biometric/ @bitwarden/team-key-management-dev apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-key-management-dev diff --git a/.github/renovate.json b/.github/renovate.json index 7f3e7464fe3..776c66af68e 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -75,10 +75,12 @@ }, { "matchPackageNames": [ + "@emotion/css", "@webcomponents/custom-elements", "concurrently", "cross-env", "del", + "lit", "nord", "patch-package", "prettier", @@ -102,6 +104,8 @@ "matchPackageNames": [ "@babel/core", "@babel/preset-env", + "@bitwarden/sdk-internal", + "@electron/fuses", "@electron/notarize", "@electron/rebuild", "@ngtools/webpack", @@ -113,7 +117,7 @@ "@types/node", "@types/node-forge", "@types/node-ipc", - "@yao-pkg", + "@yao-pkg/pkg", "babel-loader", "browserslist", "copy-webpack-plugin", @@ -133,6 +137,7 @@ "tsconfig-paths-webpack-plugin", "type-fest", "typescript", + "typescript-strict-plugin", "webpack", "webpack-cli", "webpack-dev-server", @@ -149,12 +154,13 @@ "@angular/cdk", "@angular/cli", "@angular/common", - "@angular/compiler", "@angular/compiler-cli", + "@angular/compiler", "@angular/core", "@angular/forms", + "@angular/platform-browser-dynamic", + "@angular/platform-browser", "@angular/platform", - "@angular/compiler", "@angular/router", "@compodoc/compodoc", "@ng-select/ng-select", @@ -162,8 +168,11 @@ "@storybook/addon-actions", "@storybook/addon-designs", "@storybook/addon-essentials", + "@storybook/addon-interactions", "@storybook/addon-links", "@storybook/angular", + "@storybook/manager-api", + "@storybook/theming", "@types/react", "autoprefixer", "bootstrap", @@ -186,7 +195,9 @@ "matchPackageNames": [ "@angular-eslint/eslint-plugin", "@angular-eslint/eslint-plugin-template", + "@angular-eslint/schematics", "@angular-eslint/template-parser", + "@angular/elements", "@types/jest", "@typescript-eslint/eslint-plugin", "@typescript-eslint/parser", @@ -199,6 +210,7 @@ "eslint-plugin-storybook", "eslint-plugin-tailwindcss", "husky", + "jest-extended", "jest-junit", "jest-mock-extended", "jest-preset-angular", diff --git a/.github/workflows/brew-bump-desktop.yml b/.github/workflows/brew-bump-desktop.yml deleted file mode 100644 index 1b3c99128bf..00000000000 --- a/.github/workflows/brew-bump-desktop.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Bump Desktop Cask - -on: - push: - tags: - - desktop-v** - workflow_dispatch: - -defaults: - run: - shell: bash - -jobs: - update-desktop-cask: - name: Update Bitwarden Desktop Cask - runs-on: macos-13 - steps: - - name: Login to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "brew-bump-workflow-pat" - - - name: Update Homebrew cask - uses: macauley/action-homebrew-bump-cask@445c42390d790569d938f9068d01af39ca030feb # v1.0.0 - with: - # Required, custom GitHub access token with the 'public_repo' and 'workflow' scopes - token: ${{ steps.retrieve-secrets.outputs.brew-bump-workflow-pat }} - org: bitwarden - tap: Homebrew/homebrew-cask - cask: bitwarden - tag: ${{ github.ref }} - revision: ${{ github.sha }} - force: true - dryrun: true diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index aa62d602ad8..64cbaa0c7f1 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -196,11 +196,7 @@ jobs: - name: "edge" npm_command: "dist:edge" archive_name: "dist-edge.zip" - artifact_name: "dist-edge" - - name: "edge-mv3" - npm_command: "dist:edge:mv3" - archive_name: "dist-edge.zip" - artifact_name: "DO-NOT-USE-FOR-PROD-dist-edge-MV3" + artifact_name: "dist-edge-MV3" - name: "firefox" npm_command: "dist:firefox" archive_name: "dist-firefox.zip" @@ -212,11 +208,7 @@ jobs: - name: "opera" npm_command: "dist:opera" archive_name: "dist-opera.zip" - artifact_name: "dist-opera" - - name: "opera-mv3" - npm_command: "dist:opera:mv3" - archive_name: "dist-opera.zip" - artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3" + artifact_name: "dist-opera-MV3" steps: - name: Check out repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index 3221c7eef2f..b27d1486bd2 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -1196,6 +1196,8 @@ jobs: uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0 with: channel-id: C074F5UESQ0 + method: chat.postMessage + token: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} payload: | { "blocks": [ @@ -1209,7 +1211,6 @@ jobs: ] } env: - SLACK_BOT_TOKEN: ${{ steps.retrieve-slack-secret.outputs.slack-bot-token }} BUILD_NUMBER: ${{ needs.setup.outputs.build_number }} diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index b5e84ff875b..9b890491282 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -63,14 +63,14 @@ jobs: runs-on: ubuntu-22.04 outputs: environment: ${{ steps.config.outputs.environment }} - environment-url: ${{ steps.config.outputs.environment-url }} - environment-name: ${{ steps.config.outputs.environment-name }} - environment-artifact: ${{ steps.config.outputs.environment-artifact }} - azure-login-creds: ${{ steps.config.outputs.azure-login-creds }} - retrieve-secrets-keyvault: ${{ steps.config.outputs.retrieve-secrets-keyvault }} - sync-utility: ${{ steps.config.outputs.sync-utility }} - sync-delete-destination-files: ${{ steps.config.outputs.sync-delete-destination-files }} - slack-channel-name: ${{ steps.config.outputs.slack-channel-name }} + environment_url: ${{ steps.config.outputs.environment_url }} + environment_name: ${{ steps.config.outputs.environment_name }} + environment_artifact: ${{ steps.config.outputs.environment_artifact }} + azure_login_creds: ${{ steps.config.outputs.azure_login_creds }} + retrive_secrets_keyvault: ${{ steps.config.outputs.retrive_secrets_keyvault }} + sync_utility: ${{ steps.config.outputs.sync_utility }} + sync_delete_destination_files: ${{ steps.config.outputs.sync_delete_destination_files }} + slack_channel_name: ${{ steps.config.outputs.slack_channel_name }} steps: - name: Configure id: config @@ -81,48 +81,48 @@ jobs: case ${{ inputs.environment }} in "USQA") - echo "azure-login-creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-rlktusqa-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-QA.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "EUQA") - echo "azure-login-creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-qa" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_QA_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvaulteu-westeurope-qa" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euqa.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU QA Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-qa" >> $GITHUB_OUTPUT ;; "USPROD") - echo "azure-login-creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_PROD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=bw-webvault-klrt-kv" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-COMMERCIAL.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.com" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "EUPROD") - echo "azure-login-creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-prd" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_EU_PRD_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-westeurope-prod" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-euprd.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - EU Production Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.bitwarden.eu" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-prd" >> $GITHUB_OUTPUT ;; "USDEV") - echo "azure-login-creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT - echo "retrieve-secrets-keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT - echo "environment-artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT - echo "environment-name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT - echo "environment-url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT - echo "slack-channel-name=alerts-deploy-dev" >> $GITHUB_OUTPUT + echo "azure_login_creds=AZURE_KV_US_DEV_SERVICE_PRINCIPAL" >> $GITHUB_OUTPUT + echo "retrive_secrets_keyvault=webvault-eastus-dev" >> $GITHUB_OUTPUT + echo "environment_artifact=web-*-cloud-usdev.zip" >> $GITHUB_OUTPUT + echo "environment_name=Web Vault - US Development Cloud" >> $GITHUB_OUTPUT + echo "environment_url=http://vault.$ENV_NAME_LOWER.bitwarden.pw" >> $GITHUB_OUTPUT + echo "slack_channel_name=alerts-deploy-dev" >> $GITHUB_OUTPUT ;; esac # Set the sync utility to use for deployment to the environment (az-sync or azcopy) - echo "sync-utility=azcopy" >> $GITHUB_OUTPUT + echo "sync_utility=azcopy" >> $GITHUB_OUTPUT - name: Environment Protection env: @@ -168,10 +168,10 @@ jobs: fi approval: - name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} + name: Approval for Deployment to ${{ needs.setup.outputs.environment_name }} needs: setup runs-on: ubuntu-22.04 - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} steps: - name: Success Code run: exit 0 @@ -181,9 +181,9 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} outputs: - artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} + artifact_build_commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' if: ${{ inputs.build-web-run-id }} @@ -242,7 +242,7 @@ jobs: run: | # If run-id was used, get the commit from the download-latest-artifacts-run-id step if [ "${{ inputs.build-web-run-id }}" ]; then - echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts-run-id.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT elif [ "${{ steps.download-latest-artifacts.outcome }}" == "failure" ]; then # If the download-latest-artifacts step failed, query the GH API to get the commit SHA of the artifact that was just built with trigger-build-web. @@ -251,7 +251,7 @@ jobs: else # Set the commit to the output of step download-latest-artifacts. - echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact_build_commit }}" >> $GITHUB_OUTPUT fi notify-start: @@ -271,11 +271,11 @@ jobs: id: slack-message with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} - slack-channel: ${{ needs.setup.outputs.slack-channel-name }} + slack-channel: ${{ needs.setup.outputs.slack_channel_name }} event: 'start' - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} @@ -287,7 +287,7 @@ jobs: - name: Display commit SHA run: | REPO_URL="https://github.com/bitwarden/clients/commit" - COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact_build_commit }}" echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY azure-deploy: @@ -299,9 +299,9 @@ jobs: runs-on: ubuntu-22.04 env: _ENVIRONMENT: ${{ needs.setup.outputs.environment }} - _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment-url }} - _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment-name }} - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} + _ENVIRONMENT_URL: ${{ needs.setup.outputs.environment_url }} + _ENVIRONMENT_NAME: ${{ needs.setup.outputs.environment_name }} + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment_artifact }} steps: - name: Create GitHub deployment uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7 @@ -309,31 +309,31 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' - ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} + ref: ${{ needs.artifact-check.outputs.artifact_build_commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 with: - creds: ${{ secrets[needs.setup.outputs.azure-login-creds] }} + creds: ${{ secrets[needs.setup.outputs.azure_login_creds] }} - name: Retrieve Storage Account connection string for az sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} id: retrieve-secrets-az-sync uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-dev-key-temp" - name: Retrieve Storage Account name and SPN credentials for azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} id: retrieve-secrets-azcopy uses: bitwarden/gh-actions/get-keyvault-secrets@main with: - keyvault: ${{ needs.setup.outputs.retrieve-secrets-keyvault }} + keyvault: ${{ needs.setup.outputs.retrive_secrets_keyvault }} secrets: "sa-bitwarden-web-vault-name,sp-bitwarden-web-vault-password,sp-bitwarden-web-vault-appid,sp-bitwarden-web-vault-tenant" - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' @@ -363,7 +363,7 @@ jobs: run: unzip ${{ env._ENVIRONMENT_ARTIFACT }} - name: Sync to Azure Storage Account using az storage blob sync - if: ${{ needs.setup.outputs.sync-utility == 'az-sync' }} + if: ${{ needs.setup.outputs.sync_utility == 'az-sync' }} working-directory: apps/web run: | az storage blob sync \ @@ -373,7 +373,7 @@ jobs: --delete-destination=${{ inputs.force-delete-destination }} - name: Sync to Azure Storage Account using azcopy - if: ${{ needs.setup.outputs.sync-utility == 'azcopy' }} + if: ${{ needs.setup.outputs.sync_utility == 'azcopy' }} working-directory: apps/web env: AZCOPY_AUTO_LOGIN_TYPE: SPN @@ -397,7 +397,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'success' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -406,7 +406,7 @@ jobs: uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 with: token: '${{ secrets.GITHUB_TOKEN }}' - environment-url: ${{ env._ENVIRONMENT_URL }} + environment_url: ${{ env._ENVIRONMENT_URL }} state: 'failure' deployment-id: ${{ steps.deployment.outputs.deployment_id }} @@ -424,11 +424,11 @@ jobs: uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: project: Clients - environment: ${{ needs.setup.outputs.environment-name }} + environment: ${{ needs.setup.outputs.environment_name }} tag: ${{ inputs.branch-or-tag }} slack-channel: ${{ needs.notify-start.outputs.channel_id }} event: ${{ needs.azure-deploy.result }} url: https://github.com/bitwarden/clients/actions/runs/${{ github.run_id }} - commit-sha: ${{ needs.artifact-check.outputs.artifact-build-commit }} + commit-sha: ${{ needs.artifact-check.outputs.artifact_build_commit }} update-ts: ${{ needs.notify-start.outputs.ts }} AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a907618bd36..867de3844e7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -54,21 +54,25 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ steps.retrieve-node-version.outputs.node_version }} + - name: Install Node dependencies + run: npm ci + + - name: Lint unowned dependencies + run: npm run lint:dep-ownership + - name: Run linter - run: | - npm ci - npm run lint + run: npm run lint rust: name: Run Rust lint on ${{ matrix.os }} - runs-on: ${{ matrix.os || 'ubuntu-latest' }} + runs-on: ${{ matrix.os || 'ubuntu-24.04' }} strategy: matrix: os: - - ubuntu-latest - - macos-latest - - windows-latest + - ubuntu-24.04 + - macos-14 + - windows-2022 steps: - name: Checkout repo diff --git a/.github/workflows/publish-cli.yml b/.github/workflows/publish-cli.yml index 0a561306797..ff85a30d3f6 100644 --- a/.github/workflows/publish-cli.yml +++ b/.github/workflows/publish-cli.yml @@ -43,8 +43,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version-output.outputs.version }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version-output.outputs.version }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} defaults: run: working-directory: . @@ -88,7 +88,7 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -125,7 +125,7 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -165,7 +165,7 @@ jobs: needs: setup if: inputs.npm_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -222,7 +222,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -230,4 +230,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/publish-desktop.yml b/.github/workflows/publish-desktop.yml index 5ef378ad439..69ccd841065 100644 --- a/.github/workflows/publish-desktop.yml +++ b/.github/workflows/publish-desktop.yml @@ -39,10 +39,10 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - tag-name: ${{ steps.version.outputs.tag_name }} - deployment-id: ${{ steps.deployment.outputs.deployment_id }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + tag_name: ${{ steps.version.outputs.tag_name }} + deployment_id: ${{ steps.deployment.outputs.deployment_id }} steps: - name: Branch check if: ${{ inputs.publish_type != 'Dry Run' }} @@ -76,7 +76,7 @@ jobs: fi - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -100,7 +100,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Production' - description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release-channel.outputs.channel }} from branch ${{ github.ref_name }}' + description: 'Deployment ${{ steps.version.outputs.version }} to channel ${{ steps.release_channel.outputs.channel }} from branch ${{ github.ref_name }}' task: release electron-blob: @@ -108,8 +108,8 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -136,7 +136,7 @@ jobs: - name: Set staged rollout percentage env: - RELEASE_CHANNEL: ${{ needs.setup.outputs.release-channel }} + RELEASE_CHANNEL: ${{ needs.setup.outputs.release_channel }} ROLLOUT_PCT: ${{ inputs.rollout_percentage }} run: | echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}.yml @@ -163,7 +163,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -171,7 +171,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} snap: name: Deploy Snap @@ -179,8 +179,8 @@ jobs: needs: setup if: inputs.snap_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -223,8 +223,8 @@ jobs: needs: setup if: inputs.choco_publish env: - _PKG_VERSION: ${{ needs.setup.outputs.release-version }} - _RELEASE_TAG: ${{ needs.setup.outputs.tag-name }} + _PKG_VERSION: ${{ needs.setup.outputs.release_version }} + _RELEASE_TAG: ${{ needs.setup.outputs.tag_name }} steps: - name: Checkout Repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -284,7 +284,7 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'success' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} - name: Update deployment status to Failure if: ${{ inputs.publish_type != 'Dry Run' && failure() }} @@ -292,4 +292,4 @@ jobs: with: token: '${{ secrets.GITHUB_TOKEN }}' state: 'failure' - deployment-id: ${{ needs.setup.outputs.deployment-id }} + deployment_id: ${{ needs.setup.outputs.deployment_id }} diff --git a/.github/workflows/release-browser.yml b/.github/workflows/release-browser.yml index 75442187516..7e8722dc79f 100644 --- a/.github/workflows/release-browser.yml +++ b/.github/workflows/release-browser.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ github.event.inputs.release_type }} project-type: ts @@ -118,7 +118,7 @@ jobs: - name: Rename build artifacts env: - PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} run: | mv browser-source.zip browser-source-$PACKAGE_VERSION.zip mv dist-chrome.zip dist-chrome-$PACKAGE_VERSION.zip @@ -130,14 +130,14 @@ jobs: if: ${{ github.event.inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 with: - artifacts: 'browser-source-${{ needs.setup.outputs.release-version }}.zip, - dist-chrome-${{ needs.setup.outputs.release-version }}.zip, - dist-opera-${{ needs.setup.outputs.release-version }}.zip, - dist-firefox-${{ needs.setup.outputs.release-version }}.zip, - dist-edge-${{ needs.setup.outputs.release-version }}.zip' + artifacts: 'browser-source-${{ needs.setup.outputs.release_version }}.zip, + dist-chrome-${{ needs.setup.outputs.release_version }}.zip, + dist-opera-${{ needs.setup.outputs.release_version }}.zip, + dist-firefox-${{ needs.setup.outputs.release_version }}.zip, + dist-edge-${{ needs.setup.outputs.release_version }}.zip' commit: ${{ github.sha }} - tag: "browser-v${{ needs.setup.outputs.release-version }}" - name: "Browser v${{ needs.setup.outputs.release-version }}" + tag: "browser-v${{ needs.setup.outputs.release_version }}" + name: "Browser v${{ needs.setup.outputs.release_version }}" body: "" token: ${{ secrets.GITHUB_TOKEN }} draft: true diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 05c53f9752d..d16cd744d7d 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -23,7 +23,7 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} + release_version: ${{ steps.version.outputs.version }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -75,7 +75,7 @@ jobs: if: ${{ inputs.release_type != 'Dry Run' }} uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} with: artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt, diff --git a/.github/workflows/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index 3ec11c77852..08174dc552e 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -16,9 +16,9 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} - branch-name: ${{ steps.branch.outputs.branch-name }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} + branch_name: ${{ steps.branch.outputs.branch_name }} build_number: ${{ steps.increment-version.outputs.build_number }} node_version: ${{ steps.retrieve-node-version.outputs.node_version }} steps: @@ -47,7 +47,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: 'Initial Release' project-type: ts @@ -63,7 +63,7 @@ jobs: echo "build_number=$BUILD_NUMBER" >> $GITHUB_OUTPUT - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -102,7 +102,7 @@ jobs: git push -u origin $branch_name - echo "branch-name=$branch_name" >> $GITHUB_OUTPUT + echo "branch_name=$branch_name" >> $GITHUB_OUTPUT - name: Get Node Version id: retrieve-node-version @@ -116,7 +116,7 @@ jobs: runs-on: ubuntu-22.04 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -126,7 +126,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -195,8 +195,8 @@ jobs: - name: Upload auto-update artifact uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: - name: ${{ needs.setup.outputs.release-channel }}-linux.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml + name: ${{ needs.setup.outputs.release_channel }}-linux.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml if-no-files-found: error @@ -209,14 +209,14 @@ jobs: shell: pwsh working-directory: apps/desktop env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -385,8 +385,8 @@ jobs: - name: Upload auto-update artifact uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: - name: ${{ needs.setup.outputs.release-channel }}.yml - path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml + name: ${{ needs.setup.outputs.release_channel }}.yml + path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml if-no-files-found: error @@ -395,7 +395,7 @@ jobs: runs-on: macos-13 needs: setup env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -405,7 +405,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -529,7 +529,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -539,7 +539,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -730,8 +730,8 @@ jobs: - name: Upload auto-update artifact uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 with: - name: ${{ needs.setup.outputs.release-channel }}-mac.yml - path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml + name: ${{ needs.setup.outputs.release_channel }}-mac.yml + path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml if-no-files-found: error @@ -742,7 +742,7 @@ jobs: - setup - macos-build env: - _PACKAGE_VERSION: ${{ needs.setup.outputs.release-version }} + _PACKAGE_VERSION: ${{ needs.setup.outputs.release_version }} _NODE_VERSION: ${{ needs.setup.outputs.node_version }} NODE_OPTIONS: --max_old_space_size=4096 defaults: @@ -752,7 +752,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - ref: ${{ needs.setup.outputs.branch-name }} + ref: ${{ needs.setup.outputs.branch_name }} - name: Set up Node uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 @@ -939,7 +939,7 @@ jobs: token: '${{ secrets.GITHUB_TOKEN }}' initial-status: 'in_progress' environment: 'Desktop - Beta' - description: 'Deployment ${{ needs.setup.outputs.release-version }} to channel ${{ needs.setup.outputs.release-channel }} from branch ${{ needs.setup.outputs.branch-name }}' + description: 'Deployment ${{ needs.setup.outputs.release_version }} to channel ${{ needs.setup.outputs.release_channel }} from branch ${{ needs.setup.outputs.branch_name }}' task: release - name: Login to Azure @@ -963,7 +963,7 @@ jobs: - name: Rename .pkg to .pkg.archive env: - PKG_VERSION: ${{ needs.setup.outputs.release-version }} + PKG_VERSION: ${{ needs.setup.outputs.release_version }} working-directory: apps/desktop/artifacts run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive @@ -1020,5 +1020,5 @@ jobs: git config --global url."https://".insteadOf ssh:// - name: Remove branch env: - BRANCH: ${{ needs.setup.outputs.branch-name }} + BRANCH: ${{ needs.setup.outputs.branch_name }} run: git push origin --delete $BRANCH diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index d9394347f60..ba934235b44 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -22,8 +22,8 @@ jobs: name: Setup runs-on: ubuntu-22.04 outputs: - release-version: ${{ steps.version.outputs.version }} - release-channel: ${{ steps.release-channel.outputs.channel }} + release_version: ${{ steps.version.outputs.version }} + release_channel: ${{ steps.release_channel.outputs.channel }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -40,7 +40,7 @@ jobs: - name: Check Release Version id: version - uses: bitwarden/gh-actions/release-version-check@main + uses: bitwarden/gh-actions/release_version-check@main with: release-type: ${{ inputs.release_type }} project-type: ts @@ -49,7 +49,7 @@ jobs: monorepo-project: desktop - name: Get Version Channel - id: release-channel + id: release_channel run: | case "${{ steps.version.outputs.version }}" in *"alpha"*) @@ -97,10 +97,10 @@ jobs: - name: Create Release uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # v1.14.0 - if: ${{ steps.release-channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} + if: ${{ steps.release_channel.outputs.channel == 'latest' && github.event.inputs.release_type != 'Dry Run' }} env: PKG_VERSION: ${{ steps.version.outputs.version }} - RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} + RELEASE_CHANNEL: ${{ steps.release_channel.outputs.channel }} with: artifacts: "apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-amd64.deb, apps/desktop/artifacts/Bitwarden-${{ env.PKG_VERSION }}-x86_64.rpm, diff --git a/.github/workflows/repository-management.yml b/.github/workflows/repository-management.yml index a914a2c4a7a..ac2733e765b 100644 --- a/.github/workflows/repository-management.yml +++ b/.github/workflows/repository-management.yml @@ -461,6 +461,7 @@ jobs: - name: Check out main branch uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: + fetch-depth: 0 ref: main token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index b0874b38cbf..a09e8137b65 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -66,10 +66,9 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - name: Scan with SonarCloud - uses: sonarsource/sonarcloud-github-action@02ef91109b2d589e757aefcfb2854c2783fd7b19 # v4.0.0 + uses: sonarsource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: args: > -Dsonar.organization=${{ github.repository_owner }} diff --git a/apps/browser/package.json b/apps/browser/package.json index 3adeb292b6d..9ad1805362e 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/browser", - "version": "2025.1.0", + "version": "2025.1.1", "scripts": { "build": "npm run build:chrome", "build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 webpack", - "build:edge": "cross-env BROWSER=edge webpack", + "build:edge": "cross-env BROWSER=edge MANIFEST_VERSION=3 webpack", "build:firefox": "cross-env BROWSER=firefox webpack", - "build:opera": "cross-env BROWSER=opera webpack", + "build:opera": "cross-env BROWSER=opera MANIFEST_VERSION=3 webpack", "build:safari": "cross-env BROWSER=safari webpack", "build:watch": "npm run build:watch:chrome", "build:watch:chrome": "npm run build:chrome -- --watch", @@ -30,6 +30,7 @@ "dist:safari:mv3": "cross-env MANIFEST_VERSION=3 npm run dist:safari", "test": "jest", "test:watch": "jest --watch", - "test:watch:all": "jest --watchAll" + "test:watch:all": "jest --watchAll", + "test:clearCache": "jest --clear-cache" } } diff --git a/apps/browser/postcss.config.js b/apps/browser/postcss.config.js index c4513687e89..83e237f06e5 100644 --- a/apps/browser/postcss.config.js +++ b/apps/browser/postcss.config.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef */ +/* eslint-disable no-undef, @typescript-eslint/no-require-imports */ module.exports = { plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")], }; diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 46f119bdafe..8b4bbe23e04 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -2324,6 +2324,9 @@ "message": "النطاقات", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "النطاقات المستبعدة" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden لن يطلب حفظ تفاصيل تسجيل الدخول لهذه النطافات لجميع الحسابات مسجلة الدخول. يجب عليك تحديث الصفحة لكي تصبح التغييرات نافذة المفعول." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "الموقع $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "تم حفظ تغييرات استبعاد النطاقات" }, @@ -2789,6 +2804,20 @@ "error": { "message": "خطأ" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "إنشاء اسم المستخدم" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 51bfe95a48a..86f1d07fb3f 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -2324,6 +2324,9 @@ "message": "Domenlər", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Əngəllənmiş domenlər" + }, "excludedDomains": { "message": "İstisna edilən domenlər" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, giriş etmiş bütün hesablar üçün bu domenlərin giriş detallarını saxlamağı soruşmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." }, + "blockedDomainsDesc": { + "message": "Bu veb saytlar üçün avto-doldurma və digər əlaqəli özəlliklər təklif olunmayacaq. Dəyişikliklərin qüvvəyə minməsi üçün səhifəni təzələməlisiniz." + }, + "autofillBlockedNotice": { + "message": "Bu veb sayt üçün avto-doldurma əngəllənib. Bunu ayarlarda incələyin və ya dəyişdirin." + }, + "autofillBlockedTooltip": { + "message": "Bu veb saytda avto-doldurma əngəllənib. Ayarlarda incələyin." + }, "websiteItemLabel": { "message": "Veb sayt $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Əngəllənmiş domen dəyişiklikləri saxlanıldı" + }, "excludedDomainsSavedSuccess": { "message": "İstisna domen dəyişikliyi saxlanıldı" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Xəta" }, + "decryptionError": { + "message": "Şifrə açma xətası" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden, aşağıda sadalanan seyf element(lər)inin şifrəsini aça bilmədi." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Əlavə data itkisini önləmək üçün", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "müştəri dəstəyi ilə əlaqə saxlayın.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "İstifadəçi adı yarat" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Bu elementə düzəliş etmə icazəniz yoxdur" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Əvvəlcə PIN və ya parol ilə kilid açma tələb olunduğu üçün biometrik ilə kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrik kilid açma indi əlçatmazdır." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Yanlış konfiqurasiya edilmiş sistem fayllarına görə biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Bitwarden masaüstü tətbiqi bağlı olduğu üçün biometrik kilid açma əlçatmazdır." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Bitwarden masaüstü tətbiqində $EMAIL$ üçün fəal olmadığına görə biometrik kilid açma əlçatmazdır.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Bilinməyən bilinməyən bir səbəbə görə biometrik kilid açma əlçatmazdır." + }, "authenticating": { "message": "Kimlik doğrulama" }, diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 46c804d27d9..036b1dfcb24 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -2324,6 +2324,9 @@ "message": "Дамены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Выключаныя дамены" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Вэб-сайт $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Памылка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генерыраваць імя карыстальніка" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 7cbbdc07be4..098e2b91051 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -2324,6 +2324,9 @@ "message": "Домейни", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Блокирани домейни" + }, "excludedDomains": { "message": "Изключени домейни" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Битуорден няма да пита дали да запазва данните за вход в тези сайтове за всички регистрации, в които сте вписан(а). За да влезе правилото в сила, презаредете страницата." }, + "blockedDomainsDesc": { + "message": "Автоматичното попълване и други свързани функции няма да бъдат предлагани за тези уеб сайтове. Трябва да презаредите страницата, за да влязат в сила промените." + }, + "autofillBlockedNotice": { + "message": "Автоматичното попълване е блокирано за този уеб сайт. Можете да прегледате и промените това в настройките." + }, + "autofillBlockedTooltip": { + "message": "Автоматичното попълване е блокирано за този уеб сайт. Можете да прегледате това в настройките." + }, "websiteItemLabel": { "message": "Уеб сайт $number$ (адрес)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Промените на блокираните домейни са запазени" + }, "excludedDomainsSavedSuccess": { "message": "Промените на изключените домейни са запазени" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Грешка при дешифриране" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Битоурден не може да дешифрира елементите от трезора посочени по-долу." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Свържете се с поддръжката", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "за да избегнете загубата на данни.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генериране на потр. име" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Нямате право за редактиране на този елемент" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Отключването с биометрични данни не е налично, тъй като първо се изисква отключване чрез ПИН или парола." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Отключването с биометрични данни не е налично в момента." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Отключването с биометрични данни не е налично поради неправилно настроени системни файлове." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Отключването с биометрични данни не е налично, тъй като приложението на Биуорден за компютър не работи." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Отключването с биометрични данни не е налично, тъй като не е включено за $EMAIL$ в приложението на Битуорден за компютър.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Отключването с биометрични данни не е налично по неизвестна причина." + }, "authenticating": { "message": "Удостоверяване" }, diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 2da7b40554c..f60fc2c9683 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 9f5a610c351..aaf04da98b7 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index b38783abc82..e7113d5aab1 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -2324,6 +2324,9 @@ "message": "Dominis", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominis exclosos" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no demanarà que es guarden les dades d'inici de sessió d'aquests dominis per a tots els comptes iniciats. Heu d'actualitzar la pàgina perquè els canvis tinguen efecte." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genera un nom d'usuari" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index bcc7020ffb3..eb2f9149daf 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -2324,6 +2324,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vyloučené domény" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude žádat o uložení přihlašovacích údajů pro tyto domény pro všechny přihlášené účty. Aby se změny projevily, musíte stránku obnovit." }, + "blockedDomainsDesc": { + "message": "Automatické vyplňování a další související funkce nebudou pro tyto webové stránky nabízeny. Aby se změny projevily, musíte stránku aktualizovat." + }, + "autofillBlockedNotice": { + "message": "Automatické vyplňování je pro tento web zablokováno. Zkontrolujte to nebo to změňte v nastavení." + }, + "autofillBlockedTooltip": { + "message": "Automatické vyplňování je pro tento web zablokováno. Zkontrolujte to v nastavení." + }, "websiteItemLabel": { "message": "Webová stránka $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Změny v zablokovaných doménách byly uloženy" + }, "excludedDomainsSavedSuccess": { "message": "Vyloučené změny domény byly uloženy" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrování" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nemohl dešifrovat níže uvedené položky v trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznickou podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "abyste zabránili ztrátě dat.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Vygenerovat uživatelské jméno" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Nemáte oprávnění upravit tuto položku" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrické odemknutí je nedostupné, protože je potřeba nejprve odemknout pomocí PIN nebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrické odemknutí je momentálně nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrické odemknutí není dostupné kvůli chybnému nastavení systémových souborů." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrické odemknutí není dostupné, protože je aplikace Bitwarden zavřena." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrické odemknutí není dostupné, protože není povoleno pro $EMAIL$ v desktopové aplikaci Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrické odemknutí je momentálně z neznámého důvodu nedostupné." + }, "authenticating": { "message": "Ověřování" }, diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 3bc85b10fb5..8ad494f0bfb 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Parthau wedi'u heithrio" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Gwall" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Cynhyrchu enw defnyddiwr" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index 433cdebbb17..9008049e1a4 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -2324,6 +2324,9 @@ "message": "Domæner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokerede domæner" + }, "excludedDomains": { "message": "Ekskluderede domæner" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden vil ikke anmode om at gemme login-detaljer for disse domæner for alle indloggede konti. Siden skal opfriskes for at effektuere ændringerne." }, + "blockedDomainsDesc": { + "message": "Autofyldning og andre relaterede funktioner tilbydes ikke på disse websteder. Siden skal opdateres for at effektuere ændringerne." + }, + "autofillBlockedNotice": { + "message": "Autoudfyldning er blokeret på dette websted. Gennemgå eller ændr dette i Indstillinger." + }, + "autofillBlockedTooltip": { + "message": "Autoudfyldning er blokeret på dette websted. Gennemgå i Indstillinger." + }, "websiteItemLabel": { "message": "Websted $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blokeret domæne-ændringer gemt" + }, "excludedDomainsSavedSuccess": { "message": "Ekskluderet domæne-ændringer gemt" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Fejl" }, + "decryptionError": { + "message": "Dekrypteringsfejl" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kunne ikke dekryptere boks-emne(r) anført nedenfor." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontakt kundeservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "for at undgå yderligere tab af data.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generér brugernavn" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Ingen tilladelse til at redigere dette emne" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisk oplåsning er utilgængelig, da PIN- eller adgangskode kræves først." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisk oplåsning er p.t. utilgængelig." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisk oplåsning er utilgængelig grundet fejlopsatte systemfiler." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisk oplåsning er utilgængelig, da Bitwarden-appen er lukket." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisk oplåsning er utilgængelig, da det ikke er aktiveret for $EMAIL$ i Bitwarden computer-appen.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisk oplåsning er p.t. utilgængelig grundet en ukendt årsag." + }, "authenticating": { "message": "Godkender" }, diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index 2fc5297205a..fce84d1b431 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Gesperrte Domains" + }, "excludedDomains": { "message": "Ausgeschlossene Domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden wird für alle angemeldeten Konten nicht danach fragen Zugangsdaten für diese Domains speichern. Du musst die Seite neu laden, damit die Änderungen wirksam werden." }, + "blockedDomainsDesc": { + "message": "Automatisches Ausfüllen und andere zugehörige Funktionen werden für diese Webseiten nicht angeboten. Sie müssen die Seite aktualisieren, damit die Änderungen wirksam werden." + }, + "autofillBlockedNotice": { + "message": "Das automatische Ausfüllen ist für diese Website gesperrt. Dieses Verhalten kann in den Einstellungen überprüft oder geändert werden." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Änderungen gesperrter Domains gespeichert" + }, "excludedDomainsSavedSuccess": { "message": "Änderungen der ausgeschlossenen Domain gespeichert" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Fehler" }, + "decryptionError": { + "message": "Entschlüsselungsfehler" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden konnte den unten aufgelisteten Tresoreintrag bzw. die Tresoreinträge nicht entschlüsseln." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "um zusätzlichen Datenverlust zu vermeiden.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Benutzername generieren" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Du bist nicht berechtigt, diesen Eintrag zu bearbeiten" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da zuerst mit PIN oder Passwort entsperrt werden muss." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisches Entsperren ist derzeit nicht verfügbar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisches Entsperren ist aufgrund falsch konfigurierter Systemdateien nicht verfügbar." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da die Bitwarden Desktop-App geschlossen ist." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisches Entsperren ist nicht verfügbar, da es für $EMAIL$ in der Bitwarden Desktop-App nicht aktiviert ist.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisches Entsperren ist derzeit aus einem unbekannten Grund nicht verfügbar." + }, "authenticating": { "message": "Authentifizierung" }, diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index a33bc4d0e20..bdb0eb2c17d 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1005,7 +1005,7 @@ "message": "Λίστα στοιχείων ταυτότητας στη σελίδα Καρτέλας για εύκολη αυτόματη συμπλήρωση." }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "Κάντε κλικ στα αντικείμενα για αυτόματη συμπλήρωση στην προβολή Θησαυ/κίου" }, "clearClipboard": { "message": "Εκκαθάριση Πρόχειρου", @@ -2324,6 +2324,9 @@ "message": "Τομείς", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Αποκλεισμένοι τομείς" + }, "excludedDomains": { "message": "Εξαιρούμενοι Τομείς" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Το Bitwarden δε θα ρωτήσει για να αποθηκεύσετε τα στοιχεία σύνδεσης για αυτούς τους τομείς, για όλους τους συνδεδεμένους λογαριασμούς. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." }, + "blockedDomainsDesc": { + "message": "Η αυτόματη συμπλήρωση και άλλες σχετικές λειτουργίες δεν θα προσφερθούν για αυτούς τους ιστότοπους. Πρέπει να ανανεώσετε τη σελίδα για να τεθούν σε ισχύ οι αλλαγές." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Ιστοσελίδα $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Οι αλλαγές αποκλεισμένων τομέων αποθηκεύτηκαν" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Σφάλμα" }, + "decryptionError": { + "message": "Σφάλμα αποκρυπτογράφησης" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Το Bitwarden δεν μπόρεσε να αποκρυπτογραφήσει τα αντικείμενα θησαυ/κίου που αναφέρονται παρακάτω." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Δημιουργία ονόματος χρήστη" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Δεν έχετε δικαίωμα να επεξεργαστείτε αυτό το αντικείμενο" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή απαιτείται πρώτα το ξεκλείδωμα με PIN ή κωδικό πρόσβασης." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο προς το παρόν." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο λόγω εσφαλμένων ρυθμίσεων αρχείων συστήματος." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Το βιομετρικό ξεκλείδωμα δεν είναι διαθέσιμο επειδή η εφαρμογή Bitwarden επιφάνειας εργασίας είναι κλειστή." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Ταυτοποίηση" }, diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 85937b63304..51fb3a0a770 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2339,11 +2339,11 @@ "blockedDomainsDesc": { "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." }, - "autofillBlockedNotice": { - "message": "Autofill is blocked for this website. Review or change this in settings." + "autofillBlockedNoticeV2": { + "message": "Autofill is blocked for this website." }, - "autofillBlockedTooltip": { - "message": "Autofill is blocked on this website. Review in settings." + "autofillBlockedNoticeGuidance": { + "message": "Change this in settings" }, "websiteItemLabel": { "message": "Website $number$ (URI)", @@ -2804,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -3993,8 +4007,8 @@ "passkeyRemoved": { "message": "Passkey removed" }, - "autofillSuggestions": { - "message": "Autofill suggestions" + "itemSuggestions": { + "message": "Suggested items" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" @@ -4656,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 2c13f1b9259..7e938bb6b2c 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index b6031381d2d..70e725c3a88 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded Domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate Username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index d26716dd909..d9cd2517816 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -2324,6 +2324,9 @@ "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominios excluidos" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden no pedirá que se guarden los datos de acceso para estos dominios en todas las sesiones iniciadas. Debe actualizar la página para que los cambios surtan efecto." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generar nombre de usuario" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "No tiene permiso de editar este elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 069135ddb08..5245de4fb7e 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Väljajäetud domeenid" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Viga" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genereeri kasutajanimi" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 47a56583c64..fc875be6da0 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Kanporatutako domeinuak" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Akatsa" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Sortu erabiltzaile izena" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 2b8a5e20da1..d2362a2655f 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "دامنه های مستثنی" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "خطا" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "ایجاد نام کاربری" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 718c0631156..0dd96ef9a02 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -2324,6 +2324,9 @@ "message": "Verkkotunnukset", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Ohitettavat verkkotunnukset" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ei pyydä kirjautumistietojen tallennusta näillä verkkotunnuksilla. Koskee kaikkia kirjautuneita tilejä. Ota muutokset käyttöön päivittämällä sivu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Verkkotunnus $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Rajoitettujen verkkotunnusten muutokset tallennettiin" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Virhe" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Luo käyttäjätunnus" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Sinulla ei ole oikeutta muokata tätä kohdetta" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Todennetaan" }, diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index a504cbd3174..d0fdf1018fb 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Inilayo na Domain" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Mali" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Lumikha ng username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 9566f457861..913391d218c 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -2324,6 +2324,9 @@ "message": "Domaines", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domaines bloqués" + }, "excludedDomains": { "message": "Domaines exclus" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden ne demandera pas d'enregistrer les détails de connexion pour ces domaines pour tous les comptes connectés. Vous devez actualiser la page pour que les modifications prennent effet." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Site web $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Changements de domaines exclus enregistrés" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Erreur" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Générer un nom d'utilisateur" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Vous n'êtes pas autorisé à modifier cet élément" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authentification" }, diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index b3eaae64486..e655159f246 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -2324,6 +2324,9 @@ "message": "Dominios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Dominios excluídos" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non ofrecerá gardar contas para estes dominios en ningunha das sesións iniciadas. Recarga a páxina para que os cambios fornezan efecto." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Web $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Dominios excluídos gardados" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Xerar nome de usuario" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Non tes permiso para modificar esta entrada" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autenticando" }, diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 5101c11a653..1a3057ac291 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "שגיאה" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 10ad502be82..fd4a6612af4 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "बहिष्कृत डोमेन" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "एरर" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "उपयोगकर्ता नाम बनाएँ" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index c93922cb913..331bc109309 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -2324,6 +2324,9 @@ "message": "Domene", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izuzete domene" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Web stranica $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Spremljene promjene izuzete domene" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Pogreška" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generiraj korisničko ime" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Nemaš prava za uređivanje ove stavke" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentifikacija" }, diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 8b8c9722909..0aea2c7eced 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -2324,6 +2324,9 @@ "message": "Tartomány", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Letiltott tartományok" + }, "excludedDomains": { "message": "Kizárt domainek" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "A Bitwarden nem kéri a bejelentkezési adatok mentését ezeknél a tartományoknál az összes bejelentkezési fiókra vonatkozva. A változtatások életbe lépéséhez frissíteni kell az oldalt." }, + "blockedDomainsDesc": { + "message": "Az automatikus kitöltés és az egyéb kapcsolódó funkciók ezeken a webhelyeken nincsenek a kínálatban. A változtatások életbe lépéséhez frissíteni kell az oldalt." + }, + "autofillBlockedNotice": { + "message": "Az automatikus kitöltés le van tiltva ezen a webhelyen. Tekintsük át vagy módosítsuk ezt a beállításokban." + }, + "autofillBlockedTooltip": { + "message": "Az automatikus kitöltés le van tiltva ezen a webhelyen. Tekintsük át ezt a beállításokban." + }, "websiteItemLabel": { "message": "Webhely $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "A letiltott tartomány módosítások mentésre kerültek." + }, "excludedDomainsSavedSuccess": { "message": "A kizárt tartomány módosítások mentésre kerültek." }, @@ -2789,6 +2804,20 @@ "error": { "message": "Hiba" }, + "decryptionError": { + "message": "Visszafejtési hiba" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "A Bitwarden nem tudta visszafejteni az alább felsorolt ​​széf elemeket." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Ügyfélszolgálat elérése", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "további adatvesztés elkerülése érdekében.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Felhasználónév generálása" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Nincs jogosulltság ezen elem szerkesztéséheu." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "A biometrikus feloldás nem érhető el, mert először PIN kóddal vagy jelszóval kell feloldani." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "A biometrikus feloldás jelenleg nem érhető el." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "A biometrikus feloldás nem érhető el a rosszul konfigurált rendszerfájlok miatt." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "A biometrikus feloldás nem érhető el, mert a Bitwarden asztali alkalmazás be van zárva." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "A biometrikus feloldás nem érhető el, mert nincs engedélyezve $EMAIL$ számára a Bitwarden asztali alkalmazásban.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "A biometrikus feloldás jelenleg ismeretlen okból nem érhető el." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 7b1ad51e0b7..82776a8e82b 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -2324,6 +2324,9 @@ "message": "Domain", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domain yang Dikecualikan" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden tidak akan meminta untuk menyimpan rincian login untuk domain tersebut. Anda harus menyegarkan halaman agar perubahan diterapkan." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Situs web $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Perubahan domain yang diabaikan telah disimpan" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Galat" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Buat nama pengguna baru" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 6490a441832..04e2c4ee64f 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -2324,6 +2324,9 @@ "message": "Domini", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domini bloccati" + }, "excludedDomains": { "message": "Domini esclusi" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden non chiederà di salvare le credenziali di accesso per questi domini per tutti gli account sul dispositivo. Ricarica la pagina affinché le modifiche abbiano effetto." }, + "blockedDomainsDesc": { + "message": "Per questi siti, l'auto-completamento e funzionalità simili non saranno disponibili. Ricarica la pagina per applicare le modifiche." + }, + "autofillBlockedNotice": { + "message": "L'auto-completamento è bloccato per questo sito. Modifica questa scelta nelle impostazioni." + }, + "autofillBlockedTooltip": { + "message": "L'auto-completamento è bloccato per questo sito. Verifica nelle impostazioni." + }, "websiteItemLabel": { "message": "Sito $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Modifiche ai domini bloccati salvate" + }, "excludedDomainsSavedSuccess": { "message": "Modifiche del dominio escluso salvate" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Errore" }, + "decryptionError": { + "message": "Errore di decifrazione" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden non può decifrare gli elementi elencati di seguito." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contatta il cliente correttamente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "per evitare ulteriori perdite di dati.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Genera nome utente" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Non hai i permessi per modificare questo elemento" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Lo sblocco biometrico non è disponibile perché è necessario prima sbloccare con PIN o parola d'accesso." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Lo sblocco biometrico non è attualmente disponibile." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Lo sblocco biometrico non è disponibile a causa di file di sistema mal configurati." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Lo sblocco biometrico non è disponibile perché l'app desktop Bitwarden è chiusa." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Lo sblocco biometrico non è disponibile perché non è abilitato per $EMAIL$ nell'app desktop Bitwarden.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Lo sblocco biometrico non è attualmente disponibile per un motivo sconosciuto." + }, "authenticating": { "message": "Autenticazione" }, diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 42dd73020ec..cc1f34e4985 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -2324,6 +2324,9 @@ "message": "ドメイン", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "除外するドメイン" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden はログインしているすべてのアカウントで、これらのドメインのログイン情報を保存するよう要求しません。 変更を有効にするにはページを更新する必要があります。" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "ウェブサイト $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "除外ドメインの変更を保存しました" }, @@ -2789,6 +2804,20 @@ "error": { "message": "エラー" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "ユーザー名を生成" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "このアイテムを編集する権限がありません" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "認証中" }, diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index 5da73c6755b..50fdc6613c5 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -2324,6 +2324,9 @@ "message": "დომენები", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "შეცდომა" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "ავთენტიკაცია" }, diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 09e26c18b5b..3f9e99e5637 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "ಹೊರತುಪಡಿಸಿದ ಡೊಮೇನ್ಗಳು" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index d6da55f600d..4ac6d281b09 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -2324,6 +2324,9 @@ "message": "도메인", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "제외된 도메인" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "BItwarden은 로그인한 모든 계정에 대해 이러한 도메인에 대한 로그인 세부 정보를 저장하도록 요청하지 않습니다. 변경 사항을 적용하려면 페이지를 새로 고쳐야 합니다" }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "웹사이트 $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "제외된 도메인 변경 사항 저장됨" }, @@ -2789,6 +2804,20 @@ "error": { "message": "오류" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "아이디 생성" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "아이템을 수정할 권한이 없습니다." }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "인증 중" }, diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index eaf1cb9f9db..3c81df00f10 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -2324,6 +2324,9 @@ "message": "Domenai", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Išskirti domenai" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "„Bitwarden“ neprašys išsaugoti prisijungimo detalių šiems domenams, visose prisijungusiose paskyrose. Turite atnaujinti puslapį, kad pokyčiai pradėtų galioti." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Klaida" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generuoti vartotojo vardą" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 2c2a9c3c69c..fc682ced389 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -2324,6 +2324,9 @@ "message": "Domēna vārdi", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Liegtie domēna vārdi" + }, "excludedDomains": { "message": "Izņēmuma domēni" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nevaicās saglabāt pieteikšanās datus visiem šī domēna kontiem, kuri ir pieteikušies. Ir jāpārlādē lapa, lai iedarbotos izmaiņas." }, + "blockedDomainsDesc": { + "message": "Automātiskā aizpilde un citas saistītās iespējas šajās tīmekļvietnēs netiks piedāvātas. Ir jāatsvaidzina lapa, lai izmaiņas iedarbotos." + }, + "autofillBlockedNotice": { + "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta. Šo pārskatīt vai mainīt var iestatījumos." + }, + "autofillBlockedTooltip": { + "message": "Automātiskā aizpilde šajā tīmekļvietnē ir liegta. Šo var pārskatīt iestatījumos." + }, "websiteItemLabel": { "message": "Tīmekļvietne $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Liegtā domēna vārda izmaiņas sglabātas" + }, "excludedDomainsSavedSuccess": { "message": "Saglabātas vērā neņemto domēna vārdu izmaiņas" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Kļūda" }, + "decryptionError": { + "message": "Atšifrēšanas kļūda" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nevarēja atšifrēt zemāk uzskaitītos glabātavas vienumus." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Jāsazinās ar klientu atbalstu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "lai izvairītos no papildu datu zaudējumiem.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Izveidot lietotājvārdu" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Nav nepieciešamo atļauju, lai labotu šo vienumu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama, jo vispirms ir nepieciešama atslēgšana ar PIN vai paroli." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Atslēgšana ar biometriju nav pieejama nepareizi konfigurētu sistēmas datņu dēļ." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Atslēgšana ar biometriju nav pieejama, jo Bitwarden darbvirsmas lietotne ir aizvērta." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Atslēgšana ar biometriju nav pieejama, jo tā nav iespējota $EMAIL$ Bitwarden darbvirsmas lietotnē.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Atslēgšana ar biometriju pašlaik nav pieejama nezināma iemesla dēļ." + }, "authenticating": { "message": "Autentificē" }, diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index cd210c85ce1..4cbbfc46d6a 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index d156e6d6458..cbb0b1bdf1a 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 16469051a0c..3a12c9ae4f4 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -20,19 +20,19 @@ "message": "Opprett en konto" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "Er du ny til Bitwarden?" }, "logInWithPasskey": { - "message": "Log in with passkey" + "message": "Logg inn med passnøkkel" }, "useSingleSignOn": { "message": "Use single sign-on" }, "welcomeBack": { - "message": "Welcome back" + "message": "Velkommen tilbake" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Velg et sterkt passord" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Finish creating your account by setting a password" @@ -138,31 +138,31 @@ "message": "Kopier sikkerhetskoden" }, "copyName": { - "message": "Copy name" + "message": "Kopiér navn" }, "copyCompany": { "message": "Copy company" }, "copySSN": { - "message": "Copy Social Security number" + "message": "Kopiér fødselsnummer" }, "copyPassportNumber": { - "message": "Copy passport number" + "message": "Kopiér passnummer" }, "copyLicenseNumber": { - "message": "Copy license number" + "message": "Kopiér lisensnummer" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "Kopiér privat nøkkel" }, "copyPublicKey": { - "message": "Copy public key" + "message": "Kopiér offentlig nøkkel" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "Kopiér fingeravtrykk" }, "copyCustomField": { - "message": "Copy $FIELD$", + "message": "Kopiér $FIELD$", "placeholders": { "field": { "content": "$1", @@ -171,13 +171,13 @@ } }, "copyWebsite": { - "message": "Copy website" + "message": "Kopiér nettsted" }, "copyNotes": { - "message": "Copy notes" + "message": "Kopiér notater" }, "fill": { - "message": "Fill", + "message": "Fyll", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "Auto-utfyll identitet" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "Fyll inn verifiseringskode" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "Fyll inn verifiseringskode", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -239,13 +239,13 @@ "message": "Legg til en gjenstand" }, "accountEmail": { - "message": "Account email" + "message": "Kontoens E-postadresse" }, "requestHint": { - "message": "Request hint" + "message": "Be om et hint" }, "requestPasswordHint": { - "message": "Request password hint" + "message": "Be om passordhint" }, "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou": { "message": "Enter your account email address and your password hint will be sent to you" @@ -316,7 +316,7 @@ "message": "Logg ut" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "Om Bitwarden" }, "about": { "message": "Om" @@ -325,7 +325,7 @@ "message": "More from Bitwarden" }, "continueToBitwardenDotCom": { - "message": "Continue to bitwarden.com?" + "message": "Vil du fortsette til bitwarden.com?" }, "bitwardenForBusiness": { "message": "Bitwarden for Business" @@ -382,7 +382,7 @@ "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" }, "noFoldersAdded": { - "message": "No folders added" + "message": "Ingen mapper er lagt til" }, "createFoldersToOrganize": { "message": "Create folders to organize your vault items" @@ -475,7 +475,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "Inkluder store bokstaver", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "Inkluder små bokstaver", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -499,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "Inkluder spesialtegn", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,7 +526,7 @@ "message": "Minste antall spesialtegn" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "Unngå forvekslingsbare tegn", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { @@ -564,19 +564,19 @@ "message": "Favoritt" }, "unfavorite": { - "message": "Unfavorite" + "message": "Fjern favorittstempel" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "Gjenstand lagt til i favorittene" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "Gjenstand fjernet fra favorittene" }, "notes": { "message": "Notater" }, "privateNote": { - "message": "Private note" + "message": "Privat notat" }, "note": { "message": "Notat" @@ -597,10 +597,10 @@ "message": "Åpne" }, "launchWebsite": { - "message": "Launch website" + "message": "Åpne nettstedet" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "Åpne nettstedet $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -657,7 +657,7 @@ "message": "Your account is locked" }, "or": { - "message": "or" + "message": "eller" }, "unlock": { "message": "Lås opp" @@ -745,7 +745,7 @@ "message": "Your master password cannot be recovered if you forget it!" }, "masterPassHintLabel": { - "message": "Master password hint" + "message": "Få et hint om hovedpassordet" }, "errorOccurred": { "message": "En feil har oppstått" @@ -785,10 +785,10 @@ "message": "You have been logged in!" }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Du har vellykket logget inn" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Du kan lukke dette vinduet" }, "masterPassSent": { "message": "Vi har sendt deg en E-post med hintet til superpassordet." @@ -834,10 +834,10 @@ "message": "Bitwarden can store and fill 2-step verification codes. Select the camera icon to take a screenshot of this website's authenticator QR code, or copy and paste the key into this field." }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "Lær mer om autentisering" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Kopier autentiseringsnøkkel (TOTP)" }, "loggedOut": { "message": "Logget av" @@ -852,19 +852,19 @@ "message": "Logg inn" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "Logg inn på Bitwarden" }, "restartRegistration": { "message": "Restart registration" }, "expiredLink": { - "message": "Expired link" + "message": "Utløpt lenke" }, "pleaseRestartRegistrationOrTryLoggingIn": { "message": "Please restart registration or try logging in." }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "Du har kanskje allerede en konto" }, "logOutConfirmation": { "message": "Er du sikker på at du vil logge av?" @@ -1049,7 +1049,7 @@ "message": "Lås opp" }, "additionalOptions": { - "message": "Additional options" + "message": "Ekstra innstillinger" }, "enableContextMenuItem": { "message": "Vis alternativer for kontekstmeny" @@ -1113,7 +1113,7 @@ "message": "Set a file password to encrypt the export and import it to any Bitwarden account using the password for decryption." }, "exportTypeHeading": { - "message": "Export type" + "message": "Eksporttype" }, "accountRestricted": { "message": "Account restricted" @@ -1126,7 +1126,7 @@ "description": "WARNING (should stay in capitalized letters if the language permits)" }, "warningCapitalized": { - "message": "Warning", + "message": "Advarsel", "description": "Warning (should maintain locale-relevant capitalization)" }, "confirmVaultExport": { @@ -1206,7 +1206,7 @@ "message": "Fil" }, "fileToShare": { - "message": "File to share" + "message": "Filen som skal deles" }, "selectFile": { "message": "Velg en fil." @@ -1242,7 +1242,7 @@ "message": "1 GB med kryptert fillagring for filvedlegg." }, "premiumSignUpEmergency": { - "message": "Emergency access." + "message": "Nødtilgang." }, "premiumSignUpTwoStepOptions": { "message": "Proprietary two-step login options such as YubiKey and Duo." @@ -1413,7 +1413,7 @@ "message": "E-post" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "Skriv inn koden du har fått tilsendt på E-post." }, "selfHostedEnvironment": { "message": "Selvbetjent miljø" @@ -1466,7 +1466,7 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "Autoutfyllingsforslag" }, "showInlineMenuLabel": { "message": "Show autofill suggestions on form fields" @@ -1514,7 +1514,7 @@ "message": "Kompromitterte eller upålitelige nettsider kan utnytte auto-utfylling når du laster inn siden." }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "Lær mer om risikoer" }, "learnMoreAboutAutofill": { "message": "Lær mer om auto-utfylling" @@ -1768,10 +1768,10 @@ "message": "Identitet" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH-nøkkel" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Ny $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1780,7 +1780,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Rediger $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1789,7 +1789,7 @@ } }, "viewItemHeader": { - "message": "View $TYPE$", + "message": "Vis $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1801,7 +1801,7 @@ "message": "Passordhistorikk" }, "generatorHistory": { - "message": "Generator history" + "message": "Generatorhistorikk" }, "clearGeneratorHistoryTitle": { "message": "Clear generator history" @@ -1846,7 +1846,7 @@ "message": "Sikre notiser" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH-nøkler" }, "clear": { "message": "Tøm", @@ -1929,7 +1929,7 @@ "message": "Tøm historikk" }, "nothingToShow": { - "message": "Nothing to show" + "message": "Ingenting å vise" }, "nothingGeneratedRecently": { "message": "You haven't generated anything recently" @@ -2017,7 +2017,7 @@ "message": "Lås opp med biometri" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "Lås opp med hovedpassord" }, "awaitDesktop": { "message": "Venter på bekreftelse fra skrivebordsprogrammet" @@ -2029,7 +2029,7 @@ "message": "Lås med hovedpassordet når du starter nettleseren på nytt" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "Krev hovedpassord ved omstart av nettleseren" }, "selectOneCollection": { "message": "Du må velge minst én samling." @@ -2041,22 +2041,22 @@ "message": "Klon" }, "passwordGenerator": { - "message": "Password generator" + "message": "Passordgenerator" }, "usernameGenerator": { - "message": "Username generator" + "message": "Brukernavngenerator" }, "useThisPassword": { "message": "Bruk dette passordet" }, "useThisUsername": { - "message": "Use this username" + "message": "Bruk dette brukernavnet" }, "securePasswordGenerated": { "message": "Secure password generated! Don't forget to also update your password on the website." }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "Bruk denne generatoren", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { @@ -2096,7 +2096,7 @@ "message": "Gjenopprettet objekt" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Har du allerede en konto?" }, "vaultTimeoutLogOutConfirmation": { "message": "Hvis du logger ut, fjerner du all tilgang til hvelvet ditt og krever online godkjenning etter tidsavbrudd. Er du sikker på at du vil bruke denne innstillingen?" @@ -2108,7 +2108,7 @@ "message": "Autofyll og lagre" }, "fillAndSave": { - "message": "Fill and save" + "message": "Fyll og lagre" }, "autoFillSuccessAndSavedUri": { "message": "Autoutfylt objekt og lagret URI" @@ -2195,10 +2195,10 @@ "message": "Avslutt abonnement" }, "atAnyTime": { - "message": "at any time." + "message": "når som helst." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Ved å fortsette, samtykker du til" }, "and": { "message": "og" @@ -2324,6 +2324,9 @@ "message": "Domener", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokkerte domener" + }, "excludedDomains": { "message": "Ekskluderte domener" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,11 +2363,14 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, "limitSendViews": { - "message": "Limit views" + "message": "Begrens visninger" }, "limitSendViewsHint": { "message": "No one can view this Send after the limit is reached.", @@ -2383,7 +2398,7 @@ "message": "Tekst" }, "sendTypeTextToShare": { - "message": "Text to share" + "message": "Teksten som skal deles" }, "sendTypeFile": { "message": "Fil" @@ -2619,7 +2634,7 @@ "message": "Velg mappe …" }, "noFoldersFound": { - "message": "No folders found", + "message": "Ingen mapper ble funnet", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2631,7 +2646,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "cardMetrics": { - "message": "out of $TOTAL$", + "message": "av $TOTAL$", "placeholders": { "total": { "content": "$1", @@ -2789,6 +2804,20 @@ "error": { "message": "Feil" }, + "decryptionError": { + "message": "Dekrypteringsfeil" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generer brukernavn" }, @@ -2796,7 +2825,7 @@ "message": "Generate email" }, "spinboxBoundariesHint": { - "message": "Value must be between $MIN$ and $MAX$.", + "message": "Verdien må være mellom $MIN$ og $MAX$.", "description": "Explains spin box minimum and maximum values to the user", "placeholders": { "min": { @@ -2861,7 +2890,7 @@ "message": "Generer et e-postalias med en ekstern videresendingstjeneste." }, "forwarderDomainName": { - "message": "Email domain", + "message": "E-postdomene", "description": "Labels the domain name email forwarder service option" }, "forwarderDomainNameHint": { @@ -2883,11 +2912,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Nettsted: $WEBSITE$. Generert av Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2931,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Ugyldig $SERVICENAME$-domene.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -3068,16 +3097,16 @@ "message": "Send varslingen på nytt" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "Vis alle påloggingsalternativer" }, "notificationSentDevice": { "message": "Et varsel er sendt til enheten din." }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "Et varsel ble sendt til enheten din" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" @@ -3086,7 +3115,7 @@ "message": "You will be notified once the request is approved" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "Trenger du et annet alternativ?" }, "loginInitiated": { "message": "Login initiated" @@ -3188,7 +3217,7 @@ "message": "Device approval required. Select an approval option below:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "Enhetsgodkjennelse kreves" }, "selectAnApprovalOptionBelow": { "message": "Select an approval option below" @@ -3212,7 +3241,7 @@ "message": "Organization SSO identifier is required." }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Oppretter en konto på" }, "checkYourEmail": { "message": "Check your email" @@ -3230,7 +3259,7 @@ "message": "Gå tilbake" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "for å redigere E-postadressen din." }, "eu": { "message": "EU", @@ -3258,7 +3287,7 @@ "message": "Du vil bli varslet når det er godkjent." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Har du problemer med å logge inn?" }, "loginApproved": { "message": "Innlogging godkjent" @@ -3270,7 +3299,7 @@ "message": "Active user email not found. Logging you out." }, "deviceTrusted": { - "message": "Device trusted" + "message": "Enheten er betrodd" }, "sendsNoItemsTitle": { "message": "No active Sends", @@ -3421,7 +3450,7 @@ "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { - "message": "Error importing. Check console for details.", + "message": "Feil under importering. Sjekk loggkonsollen for detaljer.", "description": "Notification message for when an import has failed." }, "importNetworkError": { @@ -3458,7 +3487,7 @@ "description": "Screen reader and tool tip label for the overlay button" }, "bitwardenVault": { - "message": "Bitwarden autofill menu", + "message": "Bitwardens autoutfyllingsmeny", "description": "Page title in overlay" }, "unlockYourAccountToViewMatchingLogins": { @@ -3514,7 +3543,7 @@ "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "Nytt kort", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { @@ -3544,7 +3573,7 @@ "description": "Used for the header of the import dialog, the import button and within the file-password-prompt" }, "importError": { - "message": "Import error" + "message": "Importeringsfeil" }, "importErrorDesc": { "message": "There was a problem with the data you tried to import. Please resolve the errors listed below in your source file and try again." @@ -3708,16 +3737,16 @@ "message": "Vault data exported" }, "typePasskey": { - "message": "Passkey" + "message": "Passnøkkel" }, "accessing": { "message": "Accessing" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "Innlogget!" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "Passkoden vil ikke bli kopiert" }, "passkeyNotCopiedAlert": { "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" @@ -3747,10 +3776,10 @@ "message": "Bekreft" }, "savePasskey": { - "message": "Save passkey" + "message": "Lagre passnøkkel" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "Lagre passnøkkelen som en ny pålogging" }, "chooseCipherForPasskeySave": { "message": "Choose a login to save this passkey to" @@ -3813,7 +3842,7 @@ "message": "Approve the login request in your authentication app or enter a one-time passcode." }, "passcode": { - "message": "Passcode" + "message": "Passkode" }, "lastPassMasterPassword": { "message": "LastPass-hovedpassord" @@ -3853,7 +3882,7 @@ "message": "Bytt kontoer" }, "switchToAccount": { - "message": "Switch to account" + "message": "Bytt til konto" }, "activeAccount": { "message": "Aktiv konto" @@ -3874,13 +3903,13 @@ "message": "låst opp" }, "server": { - "message": "server" + "message": "tjener" }, "hostedAt": { "message": "betjent hos" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "Bruk enhets- eller maskinvarenøkkel" }, "justOnce": { "message": "Kun én gang" @@ -3902,7 +3931,7 @@ "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "Vil du fortsette til nettleserinnstillingene?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { @@ -3946,7 +3975,7 @@ "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Make default", + "message": "Gjør det til standarden", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -3954,7 +3983,7 @@ "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "Passordet ble lagret!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { @@ -3962,7 +3991,7 @@ "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "Passordet ble oppdatert!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3973,19 +4002,19 @@ "message": "Suksess" }, "removePasskey": { - "message": "Remove passkey" + "message": "Fjern passordnøkkel" }, "passkeyRemoved": { "message": "Passkey removed" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "Autoutfyllingsforslag" }, "autofillSuggestionsTip": { "message": "Save a login item for this site to autofill" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "Hvelvet ditt er tomt" }, "noItemsMatchSearch": { "message": "No items match your search" @@ -4014,7 +4043,7 @@ } }, "moreOptionsLabel": { - "message": "More options, $ITEMNAME$", + "message": "Flere innstillinger, $ITEMNAME$", "description": "Aria label for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4024,7 +4053,7 @@ } }, "moreOptionsTitle": { - "message": "More options - $ITEMNAME$", + "message": "Flere innstillinger - $ITEMNAME$", "description": "Title for a button that opens a menu with more options for an item.", "placeholders": { "itemname": { @@ -4034,7 +4063,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "Vis gjenstand - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4044,7 +4073,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "Autoutfyll - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4057,7 +4086,7 @@ "message": "No values to copy" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Legg til i samlinger" }, "copyEmail": { "message": "Copy email" @@ -4087,7 +4116,7 @@ "message": "Error assigning target folder." }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "Vis gjenstander i $NAME$", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4097,7 +4126,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "Tilbake til $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4110,7 +4139,7 @@ "message": "Ny" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Fjern $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { @@ -4120,13 +4149,13 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "Gjenstander uten mappe" }, "itemDetails": { - "message": "Item details" + "message": "Gjenstandens detaljer" }, "itemName": { - "message": "Item name" + "message": "Gjenstandens navn" }, "cannotRemoveViewOnlyCollections": { "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", @@ -4154,10 +4183,10 @@ "message": "Tilleggsinformasjon" }, "itemHistory": { - "message": "Item history" + "message": "Gjenstandshistorikk" }, "lastEdited": { - "message": "Last edited" + "message": "Nyligst redigert" }, "ownerYou": { "message": "Owner: You" @@ -4172,10 +4201,10 @@ "message": "Last opp" }, "addAttachment": { - "message": "Add attachment" + "message": "Legg til vedlegg" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "Maksimal filstørrelse er 500 MB" }, "deleteAttachmentName": { "message": "Delete attachment $NAME$", @@ -4187,7 +4216,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "Last ned $NAME$", "placeholders": { "name": { "content": "$1", @@ -4211,10 +4240,10 @@ "message": "Filter vault" }, "filterApplied": { - "message": "One filter applied" + "message": "Ett filter er benyttet" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "$COUNT$ filtre er benyttet", "placeholders": { "count": { "content": "$1", @@ -4223,16 +4252,16 @@ } }, "personalDetails": { - "message": "Personal details" + "message": "Personlige detaljer" }, "identification": { - "message": "Identification" + "message": "Identifikasjon" }, "contactInfo": { "message": "Kontaktinfo" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "Last ned - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4241,17 +4270,17 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "kortnummeret slutter med", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { "message": "Legitimasjoner for innlogging" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "Autentiseringsnøkkel" }, "autofillOptions": { - "message": "Autofill options" + "message": "Autoutfyllings-innstillinger" }, "websiteUri": { "message": "Website (URI)" @@ -4267,7 +4296,7 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "Nettsted lagt til" }, "addWebsite": { "message": "Legg til nettsted" @@ -4276,7 +4305,7 @@ "message": "Slett nettsted" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "Standard ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4307,16 +4336,16 @@ "message": "Autofill on page load?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "Utløpt kort" }, "cardExpiredMessage": { "message": "If you've renewed it, update the card's information" }, "cardDetails": { - "message": "Card details" + "message": "Kortdetaljer" }, "cardBrandDetails": { - "message": "$BRAND$ details", + "message": "$BRAND$-detaljer", "placeholders": { "brand": { "content": "$1", @@ -4328,7 +4357,7 @@ "message": "Aktiver animasjoner" }, "showAnimations": { - "message": "Show animations" + "message": "Vis animasjoner" }, "addAccount": { "message": "Legg til konto" @@ -4340,15 +4369,15 @@ "message": "Data" }, "passkeys": { - "message": "Passkeys", + "message": "Passnøkler", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "Passord", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { - "message": "Log in with passkey", + "message": "Logg inn med passnøkkel", "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { @@ -4373,16 +4402,16 @@ } }, "addField": { - "message": "Add field" + "message": "Legg til felt" }, "add": { "message": "Legg til" }, "fieldType": { - "message": "Field type" + "message": "Felttype" }, "fieldLabel": { - "message": "Field label" + "message": "Feltetikett" }, "textHelpText": { "message": "Use text fields for data like security questions" @@ -4412,7 +4441,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "Slett $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4421,7 +4450,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ er lagt til", "placeholders": { "label": { "content": "$1", @@ -4543,7 +4572,7 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "Gjenstandens plassering" }, "fileSend": { "message": "File Send" @@ -4558,7 +4587,7 @@ "message": "Text Sends" }, "bitwardenNewLook": { - "message": "Bitwarden has a new look!" + "message": "Bitwarden har fått et nytt utseende!" }, "bitwardenNewLookDesc": { "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" @@ -4579,16 +4608,16 @@ "message": "Enterprise policy requirements have been applied to this setting" }, "sshPrivateKey": { - "message": "Private key" + "message": "Privat nøkkel" }, "sshPublicKey": { - "message": "Public key" + "message": "Offentlig nøkkel" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "Fingeravtrykk" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "Nøkkeltype" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4609,22 +4638,22 @@ "message": "Minimum custom timeout is 1 minute." }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "Ytterligere innhold er tilgjengelig" }, "fileSavedToDevice": { "message": "File saved to device. Manage from your device downloads." }, "showCharacterCount": { - "message": "Show character count" + "message": "Vis tegntelleren" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "Skjul tegntelleren" }, "itemsInTrash": { - "message": "Items in trash" + "message": "Gjenstander i papirkurven" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "Ingen gjenstander i papirkurven" }, "noItemsInTrashDesc": { "message": "Items you delete will appear here and be permanently deleted after 30 days" @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autentiserer" }, @@ -4653,11 +4709,11 @@ "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "Vil du lagre påloggingen i Bitwarden?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "Mellomrom", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { @@ -4665,15 +4721,15 @@ "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "Baklengs apostrof", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "Utropstegn", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "Alfakrøll", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { @@ -4681,11 +4737,11 @@ "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "Dollartegn", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "Prosenttegn", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { @@ -4693,7 +4749,7 @@ "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "Prosenttegn", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { @@ -4701,23 +4757,23 @@ "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "Venstre parantes", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "Høyre parantes", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "Understrek", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "Bindestrek", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "Plusstegn", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { @@ -4725,19 +4781,19 @@ "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "Venstre krøllparentes", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "Høyre krøllparentes", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "Venstre firkantparantes", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "Høyre firkantparantes", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { @@ -4745,69 +4801,69 @@ "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "Skråstrek bakover", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "Kolon", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "Semikolon", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "Hermetegn", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "Apostrofe", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "Mindre enn", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "Større enn", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "Komma", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "Tidsperiode", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "Spørsmålstegn", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "Skråstrek", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "Små bokstaver" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "Store bokstaver" }, "generatedPassword": { - "message": "Generated password" + "message": "Generert passord" }, "compactMode": { - "message": "Compact mode" + "message": "Kompakt modus" }, "beta": { "message": "Beta" }, "importantNotice": { - "message": "Important notice" + "message": "Viktig melding" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "Sett opp 2-trinnspålogging" }, "newDeviceVerificationNoticeContentPage1": { "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." @@ -4828,7 +4884,7 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "Nei, det gjør jeg ikke" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { "message": "Yes, I can reliably access my email" @@ -4837,15 +4893,15 @@ "message": "Turn on two-step login" }, "changeAcctEmail": { - "message": "Change account email" + "message": "Endre kontoens E-postadresse" }, "extensionWidth": { - "message": "Extension width" + "message": "Utvidelsens bredde" }, "wide": { - "message": "Wide" + "message": "Bred" }, "extraWide": { - "message": "Extra wide" + "message": "Ekstra bred" } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index d167ba220c1..0112ded1983 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -2324,6 +2324,9 @@ "message": "Domeinen", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Geblokkeerde domeinen" + }, "excludedDomains": { "message": "Uitgesloten domeinen" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden zal voor deze domeinen niet vragen om de wachtwoorden op te slaan voor alle ingelogde accounts. Je moet de pagina verversen om de wijzigingen op te slaan." }, + "blockedDomainsDesc": { + "message": "Autofill en andere gerelateerde functies worden niet aangeboden voor deze websites. Vernieuw de pagina om de wijzigingen toe te passen." + }, + "autofillBlockedNotice": { + "message": "Automatisch invullen is geblokkeerd voor deze website. Bekijk of verander dit in de instellingen." + }, + "autofillBlockedTooltip": { + "message": "Automatisch invullen is geblokkeerd voor deze website. Bekijk in de instellingen." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Wijzigingen in geblokkeerde domeinen opgeslagen" + }, "excludedDomainsSavedSuccess": { "message": "Uitgesloten domeinwijzigingen opgeslagen" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Fout" }, + "decryptionError": { + "message": "Ontsleutelingsfout" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden kon de onderstaande kluisitem(s) niet ontsleutelen." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Neem contact op met de klantenservice", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "om extra dataverlies te voorkomen.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gebruikersnamen genereren" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Je hebt geen toestemming om dit item te bewerken" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat pincode of wachtwoordontgrendeling eerst vereist is." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometrisch ontgrendelen is niet beschikbaar vanwege verkeerd geconfigureerde systeembestanden." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat de Bitwarden-desktopapp is afgesloten." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometrisch ontgrendelen is niet beschikbaar omdat het niet is ingeschakeld voor $EMAIL$ in de Bitwarden-desktopapp.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometrisch ontgrendelen is momenteel niet beschikbaar om een onbekende reden." + }, "authenticating": { "message": "Aan het inloggen" }, diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index a429059ea7d..4b7d3a19fc4 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -2324,6 +2324,9 @@ "message": "Domeny", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Wykluczone domeny" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Strona internetowa $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Zmiany w wykluczonych domenach zapisane" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Błąd" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Wygeneruj nazwę użytkownika" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Nie masz uprawnień do edycji tego elementu" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Uwierzytelnianie" }, diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index cd0c9979103..e3409b1da52 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -2324,6 +2324,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domínios Excluídos" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não irá pedir para salvar os detalhes de credencial para estes domínios. Você deve atualizar a página para que as alterações entrem em vigor." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Mudanças de domínios excluídos salvas" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gerar Usuário" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Você não tem permissão para editar este arquivo" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Autenticando" }, diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index def50289ae6..6b3c190f0b5 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -2324,6 +2324,9 @@ "message": "Domínios", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Domínios bloqueados" + }, "excludedDomains": { "message": "Domínios excluídos" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "O Bitwarden não pedirá para guardar os detalhes de início de sessão destes domínios para todas as contas com sessão iniciada. É necessário atualizar a página para que as alterações tenham efeito." }, + "blockedDomainsDesc": { + "message": "O preenchimento automático e outras funcionalidades relacionadas não serão disponibilizados para estes sites. É necessário atualizar a página para que as alterações tenham efeito." + }, + "autofillBlockedNotice": { + "message": "O preenchimento automático está bloqueado para este site. Reveja ou altere esta opção nas definições." + }, + "autofillBlockedTooltip": { + "message": "O preenchimento automático está bloqueado neste site. Reveja nas definições." + }, "websiteItemLabel": { "message": "Site $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Alterações do domínio bloqueado guardadas" + }, "excludedDomainsSavedSuccess": { "message": "Alterações do domínio excluído guardadas" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Erro" }, + "decryptionError": { + "message": "Erro de desencriptação" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "O Bitwarden não conseguiu desencriptar o(s) item(ns) do cofre listado(s) abaixo." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contacte o serviço de apoio ao cliente", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "para evitar perdas adicionais de dados.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Gerar nome de utilizador" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Não tem permissão para editar este item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "O desbloqueio biométrico não está disponível porque o desbloqueio por PIN ou palavra-passe é necessário primeiro." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "O desbloqueio biométrico está atualmente indisponível." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "O desbloqueio biométrico não está disponível devido a ficheiros de sistema mal configurados." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "O desbloqueio biométrico não está disponível porque a app para computador Bitwarden está fechada." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "O desbloqueio biométrico não está disponível porque não está ativado para $EMAIL$ na app Bitwarden para computador.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "O desbloqueio biométrico está atualmente indisponível por um motivo desconhecido." + }, "authenticating": { "message": "A autenticar" }, diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 58c0a313f69..114c01aff44 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Domenii excluse" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Eroare" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generare nume de utilizator" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index acdee563ceb..53f31813ac5 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -2324,6 +2324,9 @@ "message": "Домены", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблокированные домены" + }, "excludedDomains": { "message": "Исключенные домены" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не будет предлагать сохранение логинов для этих доменов для всех авторизованных аккаунтов. Для вступления изменений в силу необходимо обновить страницу." }, + "blockedDomainsDesc": { + "message": "Автозаполнение и другие связанные с ним функции не будут предлагаться для этих сайтов. Чтобы изменения вступили в силу, необходимо обновить страницу." + }, + "autofillBlockedNotice": { + "message": "Автозаполнение для этого сайта заблокировано. Просмотрите или измените это в настройках." + }, + "autofillBlockedTooltip": { + "message": "Автозаполнение на этом сайте заблокировано. Просмотрите в настройках." + }, "websiteItemLabel": { "message": "Сайт $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Изменения в заблокированном домене сохранены" + }, "excludedDomainsSavedSuccess": { "message": "Изменения в исключенном домене сохранены" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Ошибка" }, + "decryptionError": { + "message": "Ошибка расшифровки" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden не удалось расшифровать элемент(ы) хранилища, перечисленные ниже." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Обратитесь в службу поддержки,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "чтобы избежать дополнительной потери данных.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Создать имя пользователя" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "У вас нет разрешения на редактирование этого элемента" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Биометрическая разблокировка недоступна, поскольку сначала требуется разблокировка с помощью PIN-кода или пароля." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Биометрическая разблокировка в настоящее время недоступна." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Биометрическая разблокировка недоступна из-за неправильно настроенных системных файлов." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Биометрическая разблокировка недоступна, поскольку Bitwarden для компьютера закрыт." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Биометрическая разблокировка недоступна, потому что она не включена для $EMAIL$ в приложении Bitwarden для компьютера.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Биометрическая разблокировка в настоящее время недоступна по неизвестной причине." + }, "authenticating": { "message": "Аутентификация" }, diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index b25c2fd30d5..56cf378344f 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "බැහැර වසම්" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 5d11227f003..08bfcc79f6a 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -2324,6 +2324,9 @@ "message": "Domény", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blokované domény" + }, "excludedDomains": { "message": "Vylúčené domény" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden nebude požadovať ukladanie prihlasovacích údajov pre tieto domény pre všetky prihlásené účty. Aby sa zmeny prejavili, musíte stránku obnoviť." }, + "blockedDomainsDesc": { + "message": "Automatické vypĺňanie a ďalšie súvisiace funkcie sa na týchto webových stránkach nebudú ponúkať. Aby sa zmeny prejavili, musíte stránku obnoviť." + }, + "autofillBlockedNotice": { + "message": "Automatické vypĺňanie je pre túto webovú stránku zablokované. Skontrolujte alebo zmeňte to v nastaveniach." + }, + "autofillBlockedTooltip": { + "message": "Automatické vypĺňanie je na tejto webovej stránke zablokované. Pozrite v nastaveniach." + }, "websiteItemLabel": { "message": "Webstránka $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Zmeny v blokovaných doménach boli uložené" + }, "excludedDomainsSavedSuccess": { "message": "Uložené zmeny vylúčenej domény" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Chyba" }, + "decryptionError": { + "message": "Chyba dešifrovania" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden nedokázal dešifrovať nižšie uvedené položky trezoru." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Kontaktujte zákaznícku podporu,", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "aby ste predišli ďalším stratám údajov.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Vygenerovať používateľské meno" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Na úpravu tejto položky nemáte oprávnenie" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné pretože je najskôr potrebné odomykanie pomocou PIN alebo hesla." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Odomykanie biometrickými údajmi je momentálne nedostupné." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Odomykanie biometrickými údajmi je nedostupné v dôsledku zle nastavených systémových súborov." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože aplikácia Bitwarden pre desktop je zatvorená." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Odomykanie biometrickými údajmi je nedostupné, pretože nie je povolené pre $EMAIL$ v aplikácii Bitwarden pre desktop.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Odomykanie biometrickými údajmi je momentálne z neznámych dôvodov nedostupné." + }, "authenticating": { "message": "Overuje sa" }, diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 490f991d252..2d2ee455415 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Izključene domene" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Napaka" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Ustvari uporabniško ime" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index d515c2a0c6b..01d95a6ed1b 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -2324,6 +2324,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Изузети домени" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden неће тражити да сачува податке за пријављивање за ове домене за све пријављене налоге. Морате освежити страницу да би промене ступиле на снагу." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Сајт $number$ (УРЛ)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Изузете промене домена су сачуване" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Грешка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генериши име" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Немате дозволу да уређујете ову ставку" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Аутентификација" }, diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 84de9bbfa05..a443a8e6b2e 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -2324,6 +2324,9 @@ "message": "Domäner", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Exkluderade domäner" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Webbplats $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Fel" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generera användarnamn" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 6ab3755c8f4..e34751eea7d 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index d9ff1c3f076..1b493de3d2c 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -2324,6 +2324,9 @@ "message": "Domains", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Excluded domains" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden will not ask to save login details for these domains for all logged in accounts. You must refresh the page for changes to take effect." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Website $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Excluded domain changes saved" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Error" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Generate username" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index e1236b3f86d..8095a9f6045 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -2324,6 +2324,9 @@ "message": "Alan adları", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Engellenen alan adları" + }, "excludedDomains": { "message": "Hariç tutulan alan adları" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden, oturum açmış tüm hesaplar için bu alan adlarının hesap bilgilerini kaydetmeyi sormayacaktır. Değişikliklerin etkili olması için sayfayı yenilemeniz gerekir." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Web sitesi $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Engelli alan adı değişiklikleri kaydedildi" + }, "excludedDomainsSavedSuccess": { "message": "Alan adı istisnası değişiklikleri kaydedildi" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Hata" }, + "decryptionError": { + "message": "Şifre çözme sorunu" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Kullanıcı adı oluştur" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Bu kaydı düzenleme yetkisine sahip değilsiniz" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Kimlik doğrulanıyor" }, diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 8e20bc56ff5..d6b0b88ead2 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -2324,6 +2324,9 @@ "message": "Домени", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Заблоковані домени" + }, "excludedDomains": { "message": "Виключені домени" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden не запитуватиме про збереження даних входу для цих доменів для всіх облікових записів, до яких виконано вхід. Потрібно оновити сторінку для застосування змін." }, + "blockedDomainsDesc": { + "message": "Автозаповнення та інші пов'язані функції не пропонуватимуться для цих вебсайтів. Вам слід оновити сторінку для застосування змін." + }, + "autofillBlockedNotice": { + "message": "Автозаповнення заблоковано для цього вебсайту. Перегляньте або змініть це в налаштуваннях." + }, + "autofillBlockedTooltip": { + "message": "Автозаповнення заблоковано на цьому вебсайті. Перевірте налаштування." + }, "websiteItemLabel": { "message": "Вебсайт $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Зміни заблокованих доменів збережено" + }, "excludedDomainsSavedSuccess": { "message": "Виняток для домену збережено" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Помилка" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Генерувати ім'я користувача" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "Вам не дозволено редагувати цей запис" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Аутентифікація" }, diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 268b12a6254..4ccdaf808f3 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -2324,6 +2324,9 @@ "message": "Các tên miền", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "Blocked domains" + }, "excludedDomains": { "message": "Tên miền đã loại trừ" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "Bitwarden sẽ không yêu cầu lưu thông tin đăng nhập cho các miền này. Bạn phải làm mới trang để các thay đổi có hiệu lực." }, + "blockedDomainsDesc": { + "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect." + }, + "autofillBlockedNotice": { + "message": "Autofill is blocked for this website. Review or change this in settings." + }, + "autofillBlockedTooltip": { + "message": "Autofill is blocked on this website. Review in settings." + }, "websiteItemLabel": { "message": "Trang Web $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "Blocked domain changes saved" + }, "excludedDomainsSavedSuccess": { "message": "Các thay đổi tên miền loại trừ đã được lưu" }, @@ -2789,6 +2804,20 @@ "error": { "message": "Lỗi" }, + "decryptionError": { + "message": "Decryption error" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden could not decrypt the vault item(s) listed below." + }, + "contactCSToAvoidDataLossPart1": { + "message": "Contact customer success", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "to avoid additional data loss.", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "Tạo tên người dùng" }, @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "You don't have permission to edit this item" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "Biometric unlock is unavailable because PIN or password unlock is required first." + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "Biometric unlock is currently unavailable." + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "Biometric unlock is unavailable due to misconfigured system files." + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed." + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "Biometric unlock is currently unavailable for an unknown reason." + }, "authenticating": { "message": "Authenticating" }, diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 69c1194af47..dd6a2286c4a 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -7,7 +7,7 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", + "message": "无论是在家中、工作中还是在旅途中,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { @@ -290,7 +290,7 @@ "message": "前往帮助中心吗?" }, "continueToHelpCenterDesc": { - "message": "访问帮助中心了解更多如何使用 Bitwarden 的信息。" + "message": "在帮助中心进一步了解如何使用 Bitwarden。" }, "continueToBrowserExtensionStore": { "message": "前往浏览器扩展商店吗?" @@ -834,7 +834,7 @@ "message": "Bitwarden 可以存储并填充两步验证码。选择相机图标来截取此网站的验证器二维码,或者手动复制并粘贴密钥到此字段。" }, "learnMoreAboutAuthenticators": { - "message": "了解更多关于验证器的信息" + "message": "进一步了解验证器" }, "copyTOTP": { "message": "复制验证器密钥 (TOTP)" @@ -855,13 +855,13 @@ "message": "登录到 Bitwarden" }, "restartRegistration": { - "message": "重新开始注册" + "message": "重启注册" }, "expiredLink": { "message": "失效链接" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "请重新注册或尝试登录。" + "message": "请重启注册或尝试登录。" }, "youMayAlreadyHaveAnAccount": { "message": "您可能已经有一个账户了" @@ -1514,10 +1514,10 @@ "message": "不完整或不信任的网站可以利用页面加载时的自动填充功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "了解更多关于风险的信息" + "message": "进一步了解风险" }, "learnMoreAboutAutofill": { - "message": "了解更多关于自动填充的信息" + "message": "进一步了解自动填充" }, "defaultAutoFillOnPageLoad": { "message": "登录项目的默认自动填充设置" @@ -2318,12 +2318,15 @@ "message": "一个组织策略正影响您的所有权选项。" }, "personalOwnershipPolicyInEffectImports": { - "message": "组织策略已阻止将项目导入您的个人密码库。" + "message": "某个组织策略已阻止将项目导入您的个人密码库。" }, "domainsTitle": { "message": "域名", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "屏蔽域名" + }, "excludedDomains": { "message": "排除域名" }, @@ -2331,7 +2334,16 @@ "message": "Bitwarden 将不会询问是否为这些域名保存登录信息。您必须刷新页面才能使更改生效。" }, "excludedDomainsDescAlt": { - "message": "Bitwarden 不会询问保存所有已登录的账户的这些域名的登录信息。必须刷新页面才能使更改生效。" + "message": "Bitwarden 将不会询问是否为所有已登录账户的这些域名保存登录信息。您必须刷新页面才能使更改生效。" + }, + "blockedDomainsDesc": { + "message": "将不会为这些网站提供自动填充和其他相关功能。您必须刷新页面才能使更改生效。" + }, + "autofillBlockedNotice": { + "message": "该网站的自动填充功能已被阻止。请在设置中查看或更改。" + }, + "autofillBlockedTooltip": { + "message": "该网站的自动填充功能已被阻止。请在设置中查看。" }, "websiteItemLabel": { "message": "网站 $number$ (URI)", @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "屏蔽域名更改已保存" + }, "excludedDomainsSavedSuccess": { "message": "排除域名更改已保存" }, @@ -2601,7 +2616,7 @@ "message": "更新主密码" }, "updateMasterPasswordWarning": { - "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" + "message": "您的主密码最近被您组织的管理员更改过。要访问密码库,您必须立即更新它。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" }, "updateWeakMasterPasswordWarning": { "message": "您的主密码不符合某一项或多项组织策略要求。要访问密码库,必须立即更新您的主密码。继续操作将使您退出当前会话,并要求您重新登录。其他设备上的活动会话可能会继续保持活动状态长达一小时。" @@ -2789,6 +2804,20 @@ "error": { "message": "错误" }, + "decryptionError": { + "message": "解密错误" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 无法解密下列密码库项目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "联系客户成功团队", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "以避免额外的数据丢失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "生成用户名" }, @@ -2810,7 +2839,7 @@ } }, "passwordLengthRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个字符生成强大的密码。", + "message": " 使用 $RECOMMENDED$ 个或更多字符生成强大的密码。", "description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -2820,7 +2849,7 @@ } }, "passphraseNumWordsRecommendationHint": { - "message": " 使用 $RECOMMENDED$ 或更多个单词生成强大的密码短语。", + "message": " 使用 $RECOMMENDED$ 个或更多单词生成强大的密码短语。", "description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).", "placeholders": { "recommended": { @@ -3413,11 +3442,11 @@ "description": "Notification button text for starting a fileless import." }, "importing": { - "message": "导入中...", + "message": "正在导入...", "description": "Notification message for when an import is in progress." }, "dataSuccessfullyImported": { - "message": "数据导入成功!", + "message": "数据成功导入!", "description": "Notification message for when an import has completed successfully." }, "dataImportFailed": { @@ -3926,7 +3955,7 @@ "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "将 Bitwarden 设置为您的默认密码管理器吗?", + "message": "将 Bitwarden 设置为默认密码管理器吗?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { @@ -3934,7 +3963,7 @@ "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "将 Bitwarden 设置为您的默认密码管理器", + "message": "将 Bitwarden 设置为默认密码管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { @@ -3991,7 +4020,7 @@ "message": "没有搜索到匹配的项目" }, "clearFiltersOrTryAnother": { - "message": "清除筛选器或尝试另一个搜索词" + "message": "清除筛选或尝试另一个搜索词" }, "copyInfoTitle": { "message": "复制信息 - $ITEMNAME$", @@ -4567,7 +4596,7 @@ "message": "账户操作" }, "showNumberOfAutofillSuggestions": { - "message": "在扩展图标上显示自动填充建议的登录的数量" + "message": "在扩展图标上显示自动填充建议的登录数量" }, "showQuickCopyActions": { "message": "在密码库上显示快速复制操作" @@ -4641,6 +4670,33 @@ "noEditPermissions": { "message": "您没有编辑此项目的权限" }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "生物识别解锁不可用,因为需要先使用 PIN 或密码解锁。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物识别解锁当前不可用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由于系统文件配置错误,生物识别解锁不可用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "生物识别解锁不可用,因为 Bitwarden 桌面 App 已关闭。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "生物识别解锁不可用,因为在 Bitwarden 桌面 App 中没有为 $EMAIL$ 启用生物识别解锁。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "由于某个未知的原因,生物识别解锁当前不可用。" + }, "authenticating": { "message": "正在验证" }, @@ -4819,7 +4875,7 @@ "message": "稍后提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?", + "message": "您可以正常访问您的电子邮箱 $EMAIL$ 吗?", "placeholders": { "email": { "content": "$1", diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 4b215df6ba2..467deffd815 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -20,7 +20,7 @@ "message": "建立帳戶" }, "newToBitwarden": { - "message": "New to Bitwarden?" + "message": "第一次使用 Bitwarden?" }, "logInWithPasskey": { "message": "使用密碼金鑰登入" @@ -29,7 +29,7 @@ "message": "使用單一登入" }, "welcomeBack": { - "message": "Welcome back" + "message": "歡迎回來" }, "setAStrongPassword": { "message": "設定一個強密碼" @@ -84,7 +84,7 @@ "message": "加入組織" }, "joinOrganizationName": { - "message": "Join $ORGANIZATIONNAME$", + "message": "加入 $ORGANIZATIONNAME$", "placeholders": { "organizationName": { "content": "$1", @@ -120,7 +120,7 @@ "message": "複製密碼" }, "copyPassphrase": { - "message": "Copy passphrase" + "message": "複製密碼短語" }, "copyNote": { "message": "複製備註" @@ -153,13 +153,13 @@ "message": "複製駕照號碼" }, "copyPrivateKey": { - "message": "Copy private key" + "message": "複製私密金鑰" }, "copyPublicKey": { - "message": "Copy public key" + "message": "複製公開金鑰" }, "copyFingerprint": { - "message": "Copy fingerprint" + "message": "複製指紋" }, "copyCustomField": { "message": "複製 $FIELD$", @@ -177,7 +177,7 @@ "message": "複製備註" }, "fill": { - "message": "Fill", + "message": "填入", "description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible." }, "autoFill": { @@ -193,10 +193,10 @@ "message": "自動填入身分資訊" }, "fillVerificationCode": { - "message": "Fill verification code" + "message": "填入驗證碼" }, "fillVerificationCodeAria": { - "message": "Fill Verification Code", + "message": "填入驗證碼", "description": "Aria label for the heading displayed the inline menu for totp code autofill" }, "generatePasswordCopied": { @@ -337,22 +337,22 @@ "message": "您可以使用 Bitwarden 驗證器儲存驗證器金鑰,並為兩步驟驗證流程產生 TOTP 代碼。前往 bitwarden.com 網站以了解更多資訊。" }, "bitwardenSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Bitwarden 密鑰管理" }, "continueToSecretsManagerPageDesc": { - "message": "Securely store, manage, and share developer secrets with Bitwarden Secrets Manager. Learn more on the bitwarden.com website." + "message": "使用 Bitwarden 密鑰管理來安全儲存、管理並分享開發人員密鑰。在 bitwarden.com 網站上了解更多。" }, "passwordlessDotDev": { "message": "Passwordless.dev" }, "continueToPasswordlessDotDevPageDesc": { - "message": "Create smooth and secure login experiences free from traditional passwords with Passwordless.dev. Learn more on the bitwarden.com website." + "message": "使用 Passwordless.dev 建立流暢且安全的登入體驗,無需使用傳統密碼。在 bitwarden 網站上了解更多。" }, "freeBitwardenFamilies": { - "message": "Free Bitwarden Families" + "message": "免費的 Bitwarden 家庭方案" }, "freeBitwardenFamiliesPageDesc": { - "message": "You are eligible for Free Bitwarden Families. Redeem this offer today in the web app." + "message": "您符合免費 Bitwarden 家庭方案的資格要求。在網頁應用程式中兌換您的優惠。" }, "version": { "message": "版本" @@ -379,16 +379,16 @@ "message": "資料夾名稱" }, "folderHintText": { - "message": "Nest a folder by adding the parent folder's name followed by a “/”. Example: Social/Forums" + "message": "在資料夾名稱後面使用「/」來建立樹狀結構。\n例如:社交網路/論壇" }, "noFoldersAdded": { - "message": "No folders added" + "message": "未新增資料夾" }, "createFoldersToOrganize": { - "message": "Create folders to organize your vault items" + "message": "建立資料夾來管理您的密碼庫項目" }, "deleteFolderPermanently": { - "message": "Are you sure you want to permanently delete this folder?" + "message": "您確定要永久刪除此資料夾嗎?" }, "deleteFolder": { "message": "刪除資料夾" @@ -431,7 +431,7 @@ "message": "自動產生安全、唯一的登入密碼。" }, "bitWebVaultApp": { - "message": "Bitwarden web app" + "message": "Bitwarden 網頁應用程式" }, "importItems": { "message": "匯入項目" @@ -443,7 +443,7 @@ "message": "產生密碼" }, "generatePassphrase": { - "message": "Generate passphrase" + "message": "產生密碼短語" }, "regeneratePassword": { "message": "重新產生密碼" @@ -475,7 +475,7 @@ "description": "Card header for password generator include block" }, "uppercaseDescription": { - "message": "Include uppercase characters", + "message": "包含大寫字元", "description": "Tooltip for the password generator uppercase character checkbox" }, "uppercaseLabel": { @@ -483,7 +483,7 @@ "description": "Label for the password generator uppercase character checkbox" }, "lowercaseDescription": { - "message": "Include lowercase characters", + "message": "包含小寫字元", "description": "Full description for the password generator lowercase character checkbox" }, "lowercaseLabel": { @@ -491,7 +491,7 @@ "description": "Label for the password generator lowercase character checkbox" }, "numbersDescription": { - "message": "Include numbers", + "message": "包含數字", "description": "Full description for the password generator numbers checkbox" }, "numbersLabel": { @@ -499,7 +499,7 @@ "description": "Label for the password generator numbers checkbox" }, "specialCharactersDescription": { - "message": "Include special characters", + "message": "包含特殊字元", "description": "Full description for the password generator special characters checkbox" }, "specialCharactersLabel": { @@ -526,11 +526,11 @@ "message": "最少符號位數" }, "avoidAmbiguous": { - "message": "Avoid ambiguous characters", + "message": "避免易混淆的字元", "description": "Label for the avoid ambiguous characters checkbox." }, "generatorPolicyInEffect": { - "message": "Enterprise policy requirements have been applied to your generator options.", + "message": "企業原則之要求已在你的產生器選項中生效", "description": "Indicates that a policy limits the credential generator screen." }, "searchVault": { @@ -564,19 +564,19 @@ "message": "我的最愛" }, "unfavorite": { - "message": "Unfavorite" + "message": "取消最愛" }, "itemAddedToFavorites": { - "message": "Item added to favorites" + "message": "項目已加入到最愛" }, "itemRemovedFromFavorites": { - "message": "Item removed from favorites" + "message": "項目已從最愛中移除" }, "notes": { "message": "備註" }, "privateNote": { - "message": "Private note" + "message": "私人備註" }, "note": { "message": "備註" @@ -600,7 +600,7 @@ "message": "開啟網站" }, "launchWebsiteName": { - "message": "Launch website $ITEMNAME$", + "message": "開啟網站 $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -651,7 +651,7 @@ "message": "您的密碼庫已鎖定。請驗證身分以繼續。" }, "yourVaultIsLockedV2": { - "message": "Your vault is locked" + "message": "您的密碼庫已被鎖定" }, "yourAccountIsLocked": { "message": "您的帳戶已被鎖定。" @@ -742,7 +742,7 @@ "message": "主密碼" }, "masterPassImportant": { - "message": "Your master password cannot be recovered if you forget it!" + "message": "若您忘記主密碼,將會無法找回!" }, "masterPassHintLabel": { "message": "您已成功創建新帳戶!" @@ -782,7 +782,7 @@ "message": "您已成功創建新帳戶!" }, "youHaveBeenLoggedIn": { - "message": "You have been logged in!" + "message": "你已經登入!" }, "youSuccessfullyLoggedIn": { "message": "登入成功" @@ -797,7 +797,7 @@ "message": "必須填入驗證碼。" }, "webauthnCancelOrTimeout": { - "message": "The authentication was cancelled or took too long. Please try again." + "message": "驗證已被取消或時間超過。請再試一次。" }, "invalidVerificationCode": { "message": "無效的驗證碼" @@ -834,7 +834,7 @@ "message": "Bitwarden 可以儲存並填入兩步驟驗證碼。選擇相機圖示來截取此網站的驗證器QR code,或手動複製金鑰並貼上到此欄位。" }, "learnMoreAboutAuthenticators": { - "message": "Learn more about authenticators" + "message": "了解更多驗證程式" }, "copyTOTP": { "message": "複製驗證器金鑰 (TOTP)" @@ -852,19 +852,19 @@ "message": "登入" }, "logInToBitwarden": { - "message": "Log in to Bitwarden" + "message": "登入 Bitwarden" }, "restartRegistration": { - "message": "Restart registration" + "message": "重新啟動註冊" }, "expiredLink": { - "message": "Expired link" + "message": "過期連結" }, "pleaseRestartRegistrationOrTryLoggingIn": { - "message": "Please restart registration or try logging in." + "message": "請重新啟動註冊流程或是重試登入。" }, "youMayAlreadyHaveAnAccount": { - "message": "You may already have an account" + "message": "您可能已經有帳號" }, "logOutConfirmation": { "message": "您確定要登出嗎?" @@ -888,7 +888,7 @@ "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, "twoStepLoginConfirmationContent": { - "message": "Make your account more secure by setting up two-step login in the Bitwarden web app." + "message": "在 Bitwarden 網頁應用程式中設定兩步驟登入,讓您的帳號更加安全。" }, "twoStepLoginConfirmationTitle": { "message": "前往網頁 App 嗎?" @@ -934,7 +934,7 @@ "message": "新增 URI" }, "addDomain": { - "message": "Add domain", + "message": "新增網域", "description": "'Domain' here refers to an internet domain name (e.g. 'bitwarden.com') and the message in whole described the act of putting a domain value into the context." }, "addedItem": { @@ -1005,7 +1005,7 @@ "message": "於分頁頁面顯示身分以便於自動填入。" }, "clickToAutofillOnVault": { - "message": "Click items to autofill on Vault view" + "message": "在密碼庫檢視中點擊項目來自動填入" }, "clearClipboard": { "message": "清除剪貼簿", @@ -1317,10 +1317,10 @@ "message": "輸入驗證器應用程式提供的 6 位數驗證碼。" }, "authenticationTimeout": { - "message": "Authentication timeout" + "message": "驗證逾時" }, "authenticationSessionTimedOut": { - "message": "The authentication session timed out. Please restart the login process." + "message": "驗證工作階段因時間過久已逾時。請重試登入。" }, "enterVerificationCodeEmail": { "message": "輸入已傳送至 $EMAIL$ 的 6 位數驗證碼。", @@ -1386,17 +1386,17 @@ "message": "驗證器應用程式" }, "authenticatorAppDescV2": { - "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "message": "輸入驗證器應用程式產生的驗證碼,例如 Bitwarden 驗證器。", "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, "yubiKeyTitleV2": { - "message": "Yubico OTP Security Key" + "message": "YubiKey OTP 安全金鑰" }, "yubiKeyDesc": { "message": "使用 YubiKey 存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, "duoDescV2": { - "message": "Enter a code generated by Duo Security.", + "message": "輸入 Duo 應用程式產生的驗證碼。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1413,7 +1413,7 @@ "message": "電子郵件" }, "emailDescV2": { - "message": "Enter a code sent to your email." + "message": "輸入寄送到您電子郵件信箱的驗證碼。" }, "selfHostedEnvironment": { "message": "自我裝載環境" @@ -1422,13 +1422,13 @@ "message": "指定您內部部署的 Bitwarden 安裝之基礎 URL。" }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "指定您自建的 Bitwarden 伺服器的網域 URL。例如:https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "適用於進階設定。您可以單獨指定各個服務的網域 URL。" }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "您必須新增伺服器網域 URL 或至少一個自定義環境。" }, "customEnvironment": { "message": "自訂環境" @@ -1440,7 +1440,7 @@ "message": "伺服器 URL" }, "selfHostBaseUrl": { - "message": "Self-host server URL", + "message": "自建伺服器 URL", "description": "Label for field requesting a self-hosted integration service URL" }, "apiUrl": { @@ -1466,22 +1466,22 @@ "description": "Represents the message for allowing the user to enable the autofill overlay" }, "autofillSuggestionsSectionTitle": { - "message": "Autofill suggestions" + "message": "自動填入建議" }, "showInlineMenuLabel": { - "message": "Show autofill suggestions on form fields" + "message": "在表單欄位上顯示自動填入選單" }, "showInlineMenuIdentitiesLabel": { - "message": "Display identities as suggestions" + "message": "顯示身分建議" }, "showInlineMenuCardsLabel": { - "message": "Display cards as suggestions" + "message": "顯示信用卡建議" }, "showInlineMenuOnIconSelectionLabel": { - "message": "Display suggestions when icon is selected" + "message": "當選擇圖示時,顯示建議" }, "showInlineMenuOnFormFieldsDescAlt": { - "message": "Applies to all logged in accounts." + "message": "適用於所有已登入的帳戶。" }, "turnOffBrowserBuiltInPasswordManagerSettings": { "message": "關閉你的瀏覽器內建密碼管理器設定以避免衝突。" @@ -1502,7 +1502,7 @@ "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoadSectionTitle": { - "message": "Autofill on page load" + "message": "頁面載入時自動填入" }, "enableAutoFillOnPageLoad": { "message": "頁面載入時自動填入" @@ -1514,7 +1514,7 @@ "message": "被入侵或不被信任的網站,可能會濫用頁面載入的自動填入功能。" }, "learnMoreAboutAutofillOnPageLoadLinkText": { - "message": "Learn more about risks" + "message": "了解更多風險" }, "learnMoreAboutAutofill": { "message": "進一步瞭解「自動填入」功能" @@ -1544,13 +1544,13 @@ "message": "在側邊欄中開啟密碼庫" }, "commandAutofillLoginDesc": { - "message": "Autofill the last used login for the current website" + "message": "自動將上次使用的登入資料填入目前網站" }, "commandAutofillCardDesc": { - "message": "Autofill the last used card for the current website" + "message": "自動將上次使用的信用卡填入目前網站" }, "commandAutofillIdentityDesc": { - "message": "Autofill the last used identity for the current website" + "message": "自動將上次使用的身分資料填入目前網站" }, "commandGeneratePasswordDesc": { "message": "產生一組新的隨機密碼並將它複製到剪貼簿中。" @@ -1768,7 +1768,7 @@ "message": "身分" }, "typeSshKey": { - "message": "SSH key" + "message": "SSH 金鑰" }, "newItemHeader": { "message": "新增 $TYPE$", @@ -1801,13 +1801,13 @@ "message": "密碼歷史記錄" }, "generatorHistory": { - "message": "Generator history" + "message": "產生器歷史記錄" }, "clearGeneratorHistoryTitle": { - "message": "Clear generator history" + "message": "清除產生器歷史記錄" }, "cleargGeneratorHistoryDescription": { - "message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?" + "message": "若繼續,所有產生器曾經產生的記錄會被刪除。您確定要繼續?" }, "back": { "message": "返回" @@ -1846,7 +1846,7 @@ "message": "安全筆記" }, "sshKeys": { - "message": "SSH Keys" + "message": "SSH 金鑰" }, "clear": { "message": "清除", @@ -1929,10 +1929,10 @@ "message": "清除歷史紀錄" }, "nothingToShow": { - "message": "Nothing to show" + "message": "沒有可顯示的內容" }, "nothingGeneratedRecently": { - "message": "You haven't generated anything recently" + "message": "您最近未產生任何密碼" }, "remove": { "message": "移除" @@ -2002,7 +2002,7 @@ "message": "設定您用來解鎖 Bitwarden 的 PIN 碼。您的 PIN 設定將在您完全登出本應用程式時被重設。" }, "setYourPinCode1": { - "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden." + "message": "您的 PIN 碼會取代主密碼用來解鎖 Bitwarden。您的 PIN 碼會重置,若您完全登出 Bitwarden。" }, "pinRequired": { "message": "必須填入 PIN 碼。" @@ -2017,7 +2017,7 @@ "message": "使用生物特徵辨識解鎖" }, "unlockWithMasterPassword": { - "message": "Unlock with master password" + "message": "使用主密碼解鎖" }, "awaitDesktop": { "message": "等待來自桌面應用程式的確認" @@ -2029,7 +2029,7 @@ "message": "瀏覽器重啟後使用主密碼鎖定" }, "lockWithMasterPassOnRestart1": { - "message": "Require master password on browser restart" + "message": "瀏覽器重啟後使用主密碼鎖定" }, "selectOneCollection": { "message": "您必須至少選擇一個集合。" @@ -2041,26 +2041,26 @@ "message": "克隆" }, "passwordGenerator": { - "message": "Password generator" + "message": "密碼產生器" }, "usernameGenerator": { - "message": "Username generator" + "message": "使用者名稱產生器" }, "useThisPassword": { - "message": "Use this password" + "message": "使用此密碼" }, "useThisUsername": { - "message": "Use this username" + "message": "使用此使用者名稱" }, "securePasswordGenerated": { - "message": "Secure password generated! Don't forget to also update your password on the website." + "message": "已產生安全的密碼!請不要忘記同時更新您網站上的密碼。" }, "useGeneratorHelpTextPartOne": { - "message": "Use the generator", + "message": "使用產生器", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "useGeneratorHelpTextPartTwo": { - "message": "to create a strong unique password", + "message": "來產生高強度且唯一的密碼", "description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'" }, "vaultTimeoutAction": { @@ -2096,7 +2096,7 @@ "message": "項目已還原" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "已經有帳號了嗎?" }, "vaultTimeoutLogOutConfirmation": { "message": "選擇登出將會在密碼庫逾時後移除對密碼庫的所有存取權限,若要重新驗證則需連線網路。確定要使用此設定嗎?" @@ -2108,7 +2108,7 @@ "message": "自動填入並儲存" }, "fillAndSave": { - "message": "Fill and save" + "message": "填入並儲存" }, "autoFillSuccessAndSavedUri": { "message": "項目已自動填入並且已儲存統一資源標識符(URI)" @@ -2189,19 +2189,19 @@ "message": "您新的主密碼不符合原則要求。" }, "receiveMarketingEmailsV2": { - "message": "Get advice, announcements, and research opportunities from Bitwarden in your inbox." + "message": "獲得來自 Bitwarden 的公告、建議及研究資訊電子郵件。" }, "unsubscribe": { - "message": "Unsubscribe" + "message": "取消訂閱" }, "atAnyTime": { - "message": "at any time." + "message": "在任何時間。" }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "若是繼續,則代表您同意" }, "and": { - "message": "and" + "message": "和" }, "acceptPolicies": { "message": "選中此選取框,即表示您同意下列條款:" @@ -2222,10 +2222,10 @@ "message": "確定" }, "errorRefreshingAccessToken": { - "message": "Access Token Refresh Error" + "message": "存取權杖更新失敗" }, "errorRefreshingAccessTokenDesc": { - "message": "No refresh token or API keys found. Please try logging out and logging back in." + "message": "未找到存取權杖或 API 密鑰。請重試登出再登入。" }, "desktopSyncVerificationTitle": { "message": "桌面同步驗證" @@ -2264,10 +2264,10 @@ "message": "帳戶不相符" }, "nativeMessagingWrongUserKeyTitle": { - "message": "Biometric key missmatch" + "message": "生物辨識金鑰錯誤" }, "nativeMessagingWrongUserKeyDesc": { - "message": "Biometric unlock failed. The biometric secret key failed to unlock the vault. Please try to set up biometrics again." + "message": "生物辨識解鎖失敗。生物辨識金鑰解鎖密碼庫失敗。請嘗試重新設定生物辨識。" }, "biometricsNotEnabledTitle": { "message": "生物特徵辨識未設定" @@ -2282,16 +2282,16 @@ "message": "此裝置不支援瀏覽器生物特徵辨識。" }, "biometricsNotUnlockedTitle": { - "message": "User locked or logged out" + "message": "使用者已鎖定或登出" }, "biometricsNotUnlockedDesc": { - "message": "Please unlock this user in the desktop application and try again." + "message": "請在桌面應用程式解鎖此使用者之後再重試。" }, "biometricsNotAvailableTitle": { "message": "生物辨識解鎖不可用" }, "biometricsNotAvailableDesc": { - "message": "Biometric unlock is currently unavailable. Please try again later." + "message": "生物辨識解鎖現在無法使用。請稍後重試。" }, "biometricsFailedTitle": { "message": "生物特徵辨識失敗" @@ -2324,6 +2324,9 @@ "message": "網域", "description": "A category title describing the concept of web domains" }, + "blockedDomains": { + "message": "已封鎖的網域" + }, "excludedDomains": { "message": "排除網域" }, @@ -2333,6 +2336,15 @@ "excludedDomainsDescAlt": { "message": "對於所有已登入的帳戶,Bitwarden 不會詢問是否儲存這些網域的登入資訊。您必須重新整理頁面變更才會生效。" }, + "blockedDomainsDesc": { + "message": "自動填入及其它相關的功能無法在這些網站上使用。您必須重新整理頁面來使變更生效。" + }, + "autofillBlockedNotice": { + "message": "自動填入已在此網站被封鎖。請在設定中檢視或更改此限制。" + }, + "autofillBlockedTooltip": { + "message": "自動填入已在此網站被封鎖。請在設定中檢視。" + }, "websiteItemLabel": { "message": "網站 $number$ (URI)", "placeholders": { @@ -2351,6 +2363,9 @@ } } }, + "blockedDomainsSavedSuccess": { + "message": "已儲存封鎖的網域" + }, "excludedDomainsSavedSuccess": { "message": "例外網域更改已儲存" }, @@ -2789,6 +2804,20 @@ "error": { "message": "錯誤" }, + "decryptionError": { + "message": "解密發生錯誤" + }, + "couldNotDecryptVaultItemsBelow": { + "message": "Bitwarden 無法解密您密碼庫中下面的項目。" + }, + "contactCSToAvoidDataLossPart1": { + "message": "聯絡客戶支援部門", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, + "contactCSToAvoidDataLossPart2": { + "message": "來避免更多資料遺失。", + "description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'" + }, "generateUsername": { "message": "產生使用者名稱" }, @@ -2887,7 +2916,7 @@ "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "網站:$WEBSITE$。透過 Bitwarden 產生。", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2897,7 +2926,7 @@ } }, "forwaderInvalidToken": { - "message": "Invalid $SERVICENAME$ API token", + "message": "無效的 $SERVICENAME$ API 權杖", "description": "Displayed when the user's API token is empty or rejected by the forwarding service.", "placeholders": { "servicename": { @@ -2907,7 +2936,7 @@ } }, "forwaderInvalidTokenWithMessage": { - "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$", + "message": "無效的 $SERVICENAME$ API 權杖:$ERRORMESSAGE$", "description": "Displayed when the user's API token is rejected by the forwarding service with an error message.", "placeholders": { "servicename": { @@ -2921,7 +2950,7 @@ } }, "forwarderNoAccountId": { - "message": "Unable to obtain $SERVICENAME$ masked email account ID.", + "message": "無法獲得 $SERVICENAME$ 的轉送電子郵件帳號。", "description": "Displayed when the forwarding service fails to return an account ID.", "placeholders": { "servicename": { @@ -2931,7 +2960,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "無效的 $SERVICENAME$ 網域。", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2941,7 +2970,7 @@ } }, "forwarderNoUrl": { - "message": "Invalid $SERVICENAME$ url.", + "message": "無效的 $SERVICENAME$ URI。", "description": "Displayed when the url of the forwarding service wasn't supplied.", "placeholders": { "servicename": { @@ -2951,7 +2980,7 @@ } }, "forwarderUnknownError": { - "message": "Unknown $SERVICENAME$ error occurred.", + "message": "發生未知的 $SERVICENAME$ 錯誤。", "description": "Displayed when the forwarding service failed due to an unknown error.", "placeholders": { "servicename": { @@ -2961,7 +2990,7 @@ } }, "forwarderUnknownForwarder": { - "message": "Unknown forwarder: '$SERVICENAME$'.", + "message": "未知的轉送服務提供商:$SERVICENAME$。", "description": "Displayed when the forwarding service is not supported.", "placeholders": { "servicename": { @@ -3068,25 +3097,25 @@ "message": "重新傳送通知" }, "viewAllLogInOptions": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "viewAllLoginOptionsV1": { - "message": "View all log in options" + "message": "檢視所有登入選項" }, "notificationSentDevice": { "message": "已傳送通知至您的裝置。" }, "aNotificationWasSentToYourDevice": { - "message": "A notification was sent to your device" + "message": "已傳送通知至您的裝置" }, "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": { - "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device" + "message": "請確保您的帳號已解鎖,並且指紋短語與其他裝置一致。" }, "youWillBeNotifiedOnceTheRequestIsApproved": { - "message": "You will be notified once the request is approved" + "message": "一旦您的請求被通過,您會獲得通知。" }, "needAnotherOptionV1": { - "message": "Need another option?" + "message": "需要另一個選項嗎?" }, "loginInitiated": { "message": "登入已啟動" @@ -3146,22 +3175,22 @@ "message": "自動填入設定" }, "autofillKeyboardShortcutSectionTitle": { - "message": "Autofill shortcut" + "message": "自動填入快速鍵" }, "autofillKeyboardShortcutUpdateLabel": { - "message": "Change shortcut" + "message": "修改鍵盤快速鍵" }, "autofillKeyboardManagerShortcutsLabel": { - "message": "Manage shortcuts" + "message": "管理鍵盤快速鍵" }, "autofillShortcut": { "message": "自動填入鍵盤快速鍵" }, "autofillLoginShortcutNotSet": { - "message": "The autofill login shortcut is not set. Change this in the browser's settings." + "message": "自動填入快速鍵尚未設定。請在瀏覽器的設定中變更。" }, "autofillLoginShortcutText": { - "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.", + "message": "自動填入快速鍵是 $COMMAND$。請在瀏覽器的設定中變更。", "placeholders": { "command": { "content": "$1", @@ -3182,16 +3211,16 @@ "message": "在新視窗開啟" }, "rememberThisDeviceToMakeFutureLoginsSeamless": { - "message": "Remember this device to make future logins seamless" + "message": "記住此裝置來讓未來的登入體驗更簡易" }, "deviceApprovalRequired": { "message": "裝置需要取得核准。請在下面選擇一個核准選項:" }, "deviceApprovalRequiredV2": { - "message": "Device approval required" + "message": "需要核准裝置" }, "selectAnApprovalOptionBelow": { - "message": "Select an approval option below" + "message": "選擇下面的一個核准選項" }, "rememberThisDevice": { "message": "記住這個裝置" @@ -3212,25 +3241,25 @@ "message": "需要組織 SSO 識別碼。" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "建立帳號於" }, "checkYourEmail": { - "message": "Check your email" + "message": "檢查您的電子郵件" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "跟隨電子郵件中的連結" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "並繼續建立您的帳號" }, "noEmail": { - "message": "No email?" + "message": "沒有電子郵件?" }, "goBack": { - "message": "Go back" + "message": "返回" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "來編輯您的電子郵件位址。" }, "eu": { "message": "歐盟", @@ -3267,17 +3296,17 @@ "message": "缺少使用者電子郵件地址" }, "activeUserEmailNotFoundLoggingYouOut": { - "message": "Active user email not found. Logging you out." + "message": "未找到使用中帳號的電子郵件。正在將您登出。" }, "deviceTrusted": { "message": "裝置已信任" }, "sendsNoItemsTitle": { - "message": "No active Sends", + "message": "沒有可用的 Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendsNoItemsMessage": { - "message": "Use Send to securely share encrypted information with anyone.", + "message": "使用 Send 可以與任何人安全地共用加密資訊。", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "inputRequired": { @@ -3354,10 +3383,10 @@ } }, "singleFieldNeedsAttention": { - "message": "1 field needs your attention." + "message": "您需注意上方的 1 個欄位。" }, "multipleFieldsNeedAttention": { - "message": "$COUNT$ fields need your attention.", + "message": "您需注意上方的 $COUNT$ 個欄位。", "placeholders": { "count": { "content": "$1", @@ -3444,7 +3473,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切換側邊欄" }, "skipToContent": { "message": "跳至內容" @@ -3466,7 +3495,7 @@ "description": "Text to display in overlay when the account is locked." }, "unlockYourAccountToViewAutofillSuggestions": { - "message": "Unlock your account to view autofill suggestions", + "message": "解鎖您的帳號來查看建議的自動填入", "description": "Text to display in overlay when the account is locked." }, "unlockAccount": { @@ -3474,15 +3503,15 @@ "description": "Button text to display in overlay when the account is locked." }, "unlockAccountAria": { - "message": "Unlock your account, opens in a new window", + "message": "解鎖您的帳號,並開啟在新視窗", "description": "Screen reader text (aria-label) for unlock account button in overlay" }, "totpCodeAria": { - "message": "Time-based One-Time Password Verification Code", + "message": "基於時間的一次性驗證碼", "description": "Aria label for the totp code displayed in the inline menu for autofill" }, "totpSecondsSpanAria": { - "message": "Time remaining before current TOTP expires", + "message": "現在的 TOTP 到期剩餘時間", "description": "Aria label for the totp seconds displayed in the inline menu for autofill" }, "fillCredentialsFor": { @@ -3510,23 +3539,23 @@ "description": "Button text to display within inline menu when there are no matching items on a login field" }, "addNewLoginItemAria": { - "message": "Add new vault login item, opens in a new window", + "message": "新增新的密碼庫登入項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new login button within inline menu" }, "newCard": { - "message": "New card", + "message": "新信用卡", "description": "Button text to display within inline menu when there are no matching items on a credit card field" }, "addNewCardItemAria": { - "message": "Add new vault card item, opens in a new window", + "message": "新增新的密碼庫信用卡項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new card button within inline menu" }, "newIdentity": { - "message": "New identity", + "message": "新身份識別", "description": "Button text to display within inline menu when there are no matching items on an identity field" }, "addNewIdentityItemAria": { - "message": "Add new vault identity item, opens in a new window", + "message": "新增新的密碼庫身分項目,在新視窗中顯示", "description": "Screen reader text (aria-label) for new identity button within inline menu" }, "bitwardenOverlayMenuAvailable": { @@ -3616,19 +3645,19 @@ } }, "duoHealthCheckResultsInNullAuthUrlError": { - "message": "Error connecting with the Duo service. Use a different two-step login method or contact Duo for assistance." + "message": "連接到 Duo 服務時發生錯誤。使用不同的兩階段認證或聯繫 Duo 來獲得支援。" }, "launchDuoAndFollowStepsToFinishLoggingIn": { "message": "啟動 Duo 並依照步驟完成登入。" }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "您的帳號要求使用 Duo 兩步驟驗證登入。" }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "彈出擴充套件視窗來完成登入。" }, "popoutExtension": { - "message": "Popout extension" + "message": "彈出擴充套件視窗" }, "launchDuo": { "message": "開啟Duo" @@ -3646,7 +3675,7 @@ "message": "檔案密碼無效,請使用您當初匯出檔案時輸入的密碼。" }, "destination": { - "message": "Destination" + "message": "目的" }, "learnAboutImportOptions": { "message": "瞭解更多匯入選項" @@ -3705,16 +3734,16 @@ "message": "確認檔案密碼" }, "exportSuccess": { - "message": "Vault data exported" + "message": "密碼庫資料已匯出" }, "typePasskey": { "message": "密碼金鑰" }, "accessing": { - "message": "Accessing" + "message": "正在存取" }, "loggedInExclamation": { - "message": "Logged in!" + "message": "已登入!" }, "passkeyNotCopied": { "message": "密碼金鑰不會被複製" @@ -3738,7 +3767,7 @@ "message": "您沒有符合該網站的登入資訊。" }, "noMatchingLoginsForSite": { - "message": "No matching logins for this site" + "message": "未找到此網站的登入資訊" }, "searchSavePasskeyNewLogin": { "message": "搜尋或將密碼金鑰儲存為新的登入資訊" @@ -3877,19 +3906,19 @@ "message": "伺服器" }, "hostedAt": { - "message": "hosted at" + "message": "架設在" }, "useDeviceOrHardwareKey": { - "message": "Use your device or hardware key" + "message": "使用您的裝置或密碼金鑰" }, "justOnce": { "message": "僅此一次" }, "alwaysForThisSite": { - "message": "Always for this site" + "message": "永遠針對此網站" }, "domainAddedToExcludedDomains": { - "message": "$DOMAIN$ added to excluded domains.", + "message": "$DOMAIN$ 已新增到排除的網域。", "placeholders": { "domain": { "content": "$1", @@ -3898,51 +3927,51 @@ } }, "commonImportFormats": { - "message": "Common formats", + "message": "常見格式", "description": "Label indicating the most common import formats" }, "confirmContinueToBrowserSettingsTitle": { - "message": "Continue to browser settings?", + "message": "繼續前往瀏覽器設定?", "description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page" }, "confirmContinueToHelpCenter": { - "message": "Continue to Help Center?", + "message": "繼續前往說明中心?", "description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page" }, "confirmContinueToHelpCenterPasswordManagementContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser password management settings" }, "confirmContinueToHelpCenterKeyboardShortcutsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the Help Center's page about browser keyboard shortcut settings" }, "confirmContinueToBrowserPasswordManagementSettingsContent": { - "message": "Change your browser's autofill and password management settings.", + "message": "在您瀏覽器的偏好設定中更改自動填入及密碼管理。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's password management settings page" }, "confirmContinueToBrowserKeyboardShortcutSettingsContent": { - "message": "You can view and set extension shortcuts in your browser's settings.", + "message": "您可以在您瀏覽器的偏好設定中檢視及設定擴充套件的快速鍵。", "description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "使用 Bitwarden 作為預設的密碼管理器?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between Bitwarden autofill suggestions and your browser's.", + "message": "忽略此設定可能會導致 Bitwarden 自動填入選單與您的瀏覽器產生衝突。", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "使用 Bitwarden 作為預設的密碼管理器", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Unable to set Bitwarden as the default password manager", + "message": "無法設定 Bitwarden 作為預設的密碼管理器", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "You must grant browser privacy permissions to Bitwarden to set it as the default password manager.", + "message": "您必須同意您瀏覽器的隱私權限設定來設定 Bitwarden 為預設的密碼管理器。", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { @@ -3950,19 +3979,19 @@ "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "憑證資訊成功儲存!", "description": "Notification message for when saving credentials has succeeded." }, "passwordSaved": { - "message": "Password saved!", + "message": "密碼已儲存!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "憑證資訊成功更新!", "description": "Notification message for when updating credentials has succeeded." }, "passwordUpdated": { - "message": "Password updated!", + "message": "密碼已更新!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { @@ -3970,7 +3999,7 @@ "description": "Notification message for when saving credentials has failed." }, "success": { - "message": "Success" + "message": "成功" }, "removePasskey": { "message": "移除密碼金鑰" @@ -3979,19 +4008,19 @@ "message": "密碼金鑰已移除" }, "autofillSuggestions": { - "message": "Autofill suggestions" + "message": "自動填入建議" }, "autofillSuggestionsTip": { - "message": "Save a login item for this site to autofill" + "message": "對此網站儲存登入項目為自動填入" }, "yourVaultIsEmpty": { - "message": "Your vault is empty" + "message": "您的密碼庫是空的" }, "noItemsMatchSearch": { - "message": "No items match your search" + "message": "沒有找到相符的項目。" }, "clearFiltersOrTryAnother": { - "message": "Clear filters or try another search term" + "message": "清除過濾器或更換另一個搜尋條件" }, "copyInfoTitle": { "message": "複製資訊 - $ITEMNAME$", @@ -4004,7 +4033,7 @@ } }, "copyNoteTitle": { - "message": "Copy Note - $ITEMNAME$", + "message": "複製備註 - $ITEMNAME$", "description": "Title for a button copies a note to the clipboard.", "placeholders": { "itemname": { @@ -4034,7 +4063,7 @@ } }, "viewItemTitle": { - "message": "View item - $ITEMNAME$", + "message": "檢視項目 - $ITEMNAME$", "description": "Title for a link that opens a view for an item.", "placeholders": { "itemname": { @@ -4044,7 +4073,7 @@ } }, "autofillTitle": { - "message": "Autofill - $ITEMNAME$", + "message": "自動填入 - $ITEMNAME$", "description": "Title for a button that autofills a login item.", "placeholders": { "itemname": { @@ -4054,22 +4083,22 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "沒有資料可以複製" }, "assignToCollections": { "message": "指派至集合" }, "copyEmail": { - "message": "Copy email" + "message": "複製電子郵件地址" }, "copyPhone": { - "message": "Copy phone" + "message": "複製電話" }, "copyAddress": { - "message": "Copy address" + "message": "複製地址" }, "adminConsole": { - "message": "Admin Console" + "message": "管理控制台" }, "accountSecurity": { "message": "帳戶安全性" @@ -4081,13 +4110,13 @@ "message": "外觀" }, "errorAssigningTargetCollection": { - "message": "Error assigning target collection." + "message": "指定目標集合時發生錯誤。" }, "errorAssigningTargetFolder": { - "message": "Error assigning target folder." + "message": "指定目標資料夾時發生錯誤。" }, "viewItemsIn": { - "message": "View items in $NAME$", + "message": "檢視 $NAME$ 中的項目", "description": "Button to view the contents of a folder or collection", "placeholders": { "name": { @@ -4097,7 +4126,7 @@ } }, "backTo": { - "message": "Back to $NAME$", + "message": "回到 $NAME$", "description": "Navigate back to a previous folder or collection", "placeholders": { "name": { @@ -4107,7 +4136,7 @@ } }, "new": { - "message": "New" + "message": "新增" }, "removeItem": { "message": "移除 $NAME$", @@ -4120,16 +4149,16 @@ } }, "itemsWithNoFolder": { - "message": "Items with no folder" + "message": "不在任何資料夾中的項目" }, "itemDetails": { "message": "項目詳細資訊" }, "itemName": { - "message": "Item name" + "message": "項目名稱" }, "cannotRemoveViewOnlyCollections": { - "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。", "placeholders": { "collections": { "content": "$1", @@ -4138,47 +4167,47 @@ } }, "organizationIsDeactivated": { - "message": "Organization is deactivated" + "message": "組織已被停用" }, "owner": { - "message": "Owner" + "message": "擁有者" }, "selfOwnershipLabel": { - "message": "You", + "message": "您", "description": "Used as a label to indicate that the user is the owner of an item." }, "contactYourOrgAdmin": { - "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." + "message": "無法存取已停用組織中的項目。請聯絡您組織的擁有者以獲取協助。" }, "additionalInformation": { - "message": "Additional information" + "message": "更多資訊" }, "itemHistory": { - "message": "Item history" + "message": "項目歷史記錄" }, "lastEdited": { - "message": "Last edited" + "message": "最後編輯" }, "ownerYou": { - "message": "Owner: You" + "message": "擁有者: 您" }, "linked": { - "message": "Linked" + "message": "連結" }, "copySuccessful": { - "message": "Copy Successful" + "message": "複製成功" }, "upload": { - "message": "Upload" + "message": "上傳" }, "addAttachment": { - "message": "Add attachment" + "message": "新增附件" }, "maxFileSizeSansPunctuation": { - "message": "Maximum file size is 500 MB" + "message": "最大檔案大小為 500MB" }, "deleteAttachmentName": { - "message": "Delete attachment $NAME$", + "message": "刪除附檔 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4187,7 +4216,7 @@ } }, "downloadAttachmentName": { - "message": "Download $NAME$", + "message": "下載 $NAME$", "placeholders": { "name": { "content": "$1", @@ -4196,25 +4225,25 @@ } }, "permanentlyDeleteAttachmentConfirmation": { - "message": "Are you sure you want to permanently delete this attachment?" + "message": "您確定要永久刪除此附檔嗎?" }, "premium": { "message": "進階版" }, "freeOrgsCannotUseAttachments": { - "message": "Free organizations cannot use attachments" + "message": "免費組織無法使用附檔" }, "filters": { - "message": "Filters" + "message": "篩選器" }, "filterVault": { - "message": "Filter vault" + "message": "過濾密碼庫" }, "filterApplied": { - "message": "One filter applied" + "message": "套用了一個過濾條件" }, "filterAppliedPlural": { - "message": "$COUNT$ filters applied", + "message": "套用了 $COUNT$ 個過濾條件", "placeholders": { "count": { "content": "$1", @@ -4226,13 +4255,13 @@ "message": "個人資訊" }, "identification": { - "message": "Identification" + "message": "身份" }, "contactInfo": { - "message": "Contact info" + "message": "聯繫資訊" }, "downloadAttachment": { - "message": "Download - $ITEMNAME$", + "message": "下載 - $ITEMNAME$", "placeholders": { "itemname": { "content": "$1", @@ -4241,23 +4270,23 @@ } }, "cardNumberEndsWith": { - "message": "card number ends with", + "message": "以此號碼結尾的信用卡", "description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher." }, "loginCredentials": { - "message": "Login credentials" + "message": "登入資訊" }, "authenticatorKey": { - "message": "Authenticator key" + "message": "驗證器金鑰" }, "autofillOptions": { "message": "自動填入選項" }, "websiteUri": { - "message": "Website (URI)" + "message": "網站 (URI)" }, "websiteUriCount": { - "message": "Website (URI) $COUNT$", + "message": "網站 (URI) $COUNT$", "description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.", "placeholders": { "count": { @@ -4267,16 +4296,16 @@ } }, "websiteAdded": { - "message": "Website added" + "message": "網站已新增" }, "addWebsite": { - "message": "Add website" + "message": "新增網站" }, "deleteWebsite": { - "message": "Delete website" + "message": "刪除網站" }, "defaultLabel": { - "message": "Default ($VALUE$)", + "message": "預設 ($VALUE$)", "description": "A label that indicates the default value for a field with the current default value in parentheses.", "placeholders": { "value": { @@ -4286,7 +4315,7 @@ } }, "showMatchDetection": { - "message": "Show match detection $WEBSITE$", + "message": "顯示偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4295,7 +4324,7 @@ } }, "hideMatchDetection": { - "message": "Hide match detection $WEBSITE$", + "message": "隱藏偵測到的吻合 $WEBSITE$", "placeholders": { "website": { "content": "$1", @@ -4304,13 +4333,13 @@ } }, "autoFillOnPageLoad": { - "message": "Autofill on page load?" + "message": "在頁面載入時自動填寫?" }, "cardExpiredTitle": { - "message": "Expired card" + "message": "過期的信用卡" }, "cardExpiredMessage": { - "message": "If you've renewed it, update the card's information" + "message": "如果您已續期,請更新信用卡資訊" }, "cardDetails": { "message": "信用卡詳細資料" @@ -4334,17 +4363,17 @@ "message": "新增帳戶" }, "loading": { - "message": "Loading" + "message": "正在載入" }, "data": { - "message": "Data" + "message": "資料" }, "passkeys": { "message": "密碼金鑰", "description": "A section header for a list of passkeys." }, "passwords": { - "message": "Passwords", + "message": "密碼", "description": "A section header for a list of passwords." }, "logInWithPasskeyAriaLabel": { @@ -4352,16 +4381,16 @@ "description": "ARIA label for the inline menu button that logs in with a passkey." }, "assign": { - "message": "Assign" + "message": "指定" }, "bulkCollectionAssignmentDialogDescriptionSingular": { - "message": "Only organization members with access to these collections will be able to see the item." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentDialogDescriptionPlural": { - "message": "Only organization members with access to these collections will be able to see the items." + "message": "只有可以檢視集合的組織成員才能看到其中的項目。" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "您已經選擇 $TOTAL_COUNT$ 個項目。由於您沒有編輯權限,無法更新其中的 $READONLY_COUNT$ 個項目。", "placeholders": { "total_count": { "content": "$1", @@ -4373,37 +4402,37 @@ } }, "addField": { - "message": "Add field" + "message": "新增欄位" }, "add": { - "message": "Add" + "message": "新增" }, "fieldType": { - "message": "Field type" + "message": "欄位類型" }, "fieldLabel": { - "message": "Field label" + "message": "欄位標籤" }, "textHelpText": { - "message": "Use text fields for data like security questions" + "message": "像是安全問答的資訊可以使用文字欄位來儲存" }, "hiddenHelpText": { - "message": "Use hidden fields for sensitive data like a password" + "message": "像是密碼的機密資訊可以使用隱藏欄位來儲存" }, "checkBoxHelpText": { - "message": "Use checkboxes if you'd like to autofill a form's checkbox, like a remember email" + "message": "若您想自動填入欄位中的核取方塊,例如儲存電子郵件,可以使用核取方塊。" }, "linkedHelpText": { - "message": "Use a linked field when you are experiencing autofill issues for a specific website." + "message": "使用連結欄位若您在特定網站上遇到自動填入問題。" }, "linkedLabelHelpText": { - "message": "Enter the the field's html id, name, aria-label, or placeholder." + "message": "填入欄位的 html id、名稱、標籤或預留字元" }, "editField": { - "message": "Edit field" + "message": "編輯欄位" }, "editFieldLabel": { - "message": "Edit $LABEL$", + "message": "編輯 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4412,7 +4441,7 @@ } }, "deleteCustomField": { - "message": "Delete $LABEL$", + "message": "刪除 $LABEL$", "placeholders": { "label": { "content": "$1", @@ -4421,7 +4450,7 @@ } }, "fieldAdded": { - "message": "$LABEL$ added", + "message": "$LABEL$ 已新增", "placeholders": { "label": { "content": "$1", @@ -4430,7 +4459,7 @@ } }, "reorderToggleButton": { - "message": "Reorder $LABEL$. Use arrow key to move item up or down.", + "message": "重新排序 $LABEL$。使用方向鍵來往上或下移動。", "placeholders": { "label": { "content": "$1", @@ -4439,7 +4468,7 @@ } }, "reorderFieldUp": { - "message": "$LABEL$ moved up, position $INDEX$ of $LENGTH$", + "message": "往上移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4459,10 +4488,10 @@ "message": "選擇要指派的集合" }, "personalItemTransferWarningSingular": { - "message": "1 item will be permanently transferred to the selected organization. You will no longer own this item." + "message": "1 個項目會被永久移到選擇的組織。您將不再擁有此項目。" }, "personalItemsTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to the selected organization. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到選擇的組織。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4471,7 +4500,7 @@ } }, "personalItemWithOrgTransferWarningSingular": { - "message": "1 item will be permanently transferred to $ORG$. You will no longer own this item.", + "message": "1 個項目會被永久移到 $ORG$。您將不再擁有此項目。", "placeholders": { "org": { "content": "$1", @@ -4480,7 +4509,7 @@ } }, "personalItemsWithOrgTransferWarningPlural": { - "message": "$PERSONAL_ITEMS_COUNT$ items will be permanently transferred to $ORG$. You will no longer own these items.", + "message": "$PERSONAL_ITEMS_COUNT$ 個項目會被永久移到 $ORG$。您將不再擁有這些項目。", "placeholders": { "personal_items_count": { "content": "$1", @@ -4496,10 +4525,10 @@ "message": "指派集合成功" }, "nothingSelected": { - "message": "You have not selected anything." + "message": "您沒有選擇任何項目。" }, "movedItemsToOrg": { - "message": "Selected items moved to $ORGNAME$", + "message": "將已選取項目移動至 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4508,7 +4537,7 @@ } }, "itemsMovedToOrg": { - "message": "Items moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4517,7 +4546,7 @@ } }, "itemMovedToOrg": { - "message": "Item moved to $ORGNAME$", + "message": "項目已移到 $ORGNAME$", "placeholders": { "orgname": { "content": "$1", @@ -4526,7 +4555,7 @@ } }, "reorderFieldDown": { - "message": "$LABEL$ moved down, position $INDEX$ of $LENGTH$", + "message": "往下移動 $LABEL$,位置 $LENGTH$ 之 $INDEX$", "placeholders": { "label": { "content": "$1", @@ -4543,52 +4572,52 @@ } }, "itemLocation": { - "message": "Item Location" + "message": "項目位置" }, "fileSend": { - "message": "File Send" + "message": "檔案 Send" }, "fileSends": { - "message": "File Sends" + "message": "檔案 Send" }, "textSend": { - "message": "Text Send" + "message": "文字 Send" }, "textSends": { - "message": "Text Sends" + "message": "文字 Sends" }, "bitwardenNewLook": { - "message": "Bitwarden has a new look!" + "message": "Bitwarden 有了新外觀!" }, "bitwardenNewLookDesc": { - "message": "It's easier and more intuitive than ever to autofill and search from the Vault tab. Take a look around!" + "message": "更容易使用的自動填入及密碼庫搜尋體驗。試試看吧!" }, "accountActions": { - "message": "Account actions" + "message": "帳號動作" }, "showNumberOfAutofillSuggestions": { - "message": "Show number of login autofill suggestions on extension icon" + "message": "在擴充套件圖示上顯示自動填入建議的數量" }, "showQuickCopyActions": { - "message": "Show quick copy actions on Vault" + "message": "在密碼庫中顯示快速複製" }, "systemDefault": { - "message": "System default" + "message": "系統預設值" }, "enterprisePolicyRequirementsApplied": { - "message": "Enterprise policy requirements have been applied to this setting" + "message": "企業政策已套用至您的選項中" }, "sshPrivateKey": { - "message": "Private key" + "message": "私密金鑰" }, "sshPublicKey": { - "message": "Public key" + "message": "公共金鑰" }, "sshFingerprint": { - "message": "Fingerprint" + "message": "指紋" }, "sshKeyAlgorithm": { - "message": "Key type" + "message": "金鑰類型" }, "sshKeyAlgorithmED25519": { "message": "ED25519" @@ -4603,223 +4632,250 @@ "message": "RSA 4096-Bit" }, "retry": { - "message": "Retry" + "message": "重試" }, "vaultCustomTimeoutMinimum": { - "message": "Minimum custom timeout is 1 minute." + "message": "自訂逾時時間最小為 1 分鐘。" }, "additionalContentAvailable": { - "message": "Additional content is available" + "message": "以及更多內容" }, "fileSavedToDevice": { - "message": "File saved to device. Manage from your device downloads." + "message": "檔案已儲存到裝置。在您的裝置上管理下載檔案。" }, "showCharacterCount": { - "message": "Show character count" + "message": "顯示字元數" }, "hideCharacterCount": { - "message": "Hide character count" + "message": "隱藏字元數" }, "itemsInTrash": { - "message": "Items in trash" + "message": "在垃圾桶中的項目" }, "noItemsInTrash": { - "message": "No items in trash" + "message": "垃圾桶中沒有項目" }, "noItemsInTrashDesc": { - "message": "Items you delete will appear here and be permanently deleted after 30 days" + "message": "您刪除的項目會在此顯示,並會在 30 天之後永久刪除" }, "trashWarning": { - "message": "Items that have been in trash more than 30 days will automatically be deleted" + "message": "垃圾桶中超過 30 天的項目將會被自動刪除。" }, "restore": { - "message": "Restore" + "message": "還原" }, "deleteForever": { - "message": "Delete forever" + "message": "永遠刪除" }, "noEditPermissions": { - "message": "You don't have permission to edit this item" + "message": "你沒有權限編輯這個項目" + }, + "biometricsStatusHelptextUnlockNeeded": { + "message": "需要 PIN 碼或密碼解鎖才能使用生物辨識解鎖。" + }, + "biometricsStatusHelptextHardwareUnavailable": { + "message": "生物辨識解鎖暫時無法使用。" + }, + "biometricsStatusHelptextAutoSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextManualSetupNeeded": { + "message": "由於系統檔案不正確,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextDesktopDisconnected": { + "message": "由於 Bitwarden 桌面應用程式已關閉,生物辨識解鎖無法使用。" + }, + "biometricsStatusHelptextNotEnabledInDesktop": { + "message": "由於未 Bitwarden 桌面應用程式的 $EMAIL$ 帳號上啟動,生物辨識解鎖無法使用。", + "placeholders": { + "email": { + "content": "$1", + "example": "mail@example.com" + } + } + }, + "biometricsStatusHelptextUnavailableReasonUnknown": { + "message": "基於不明原因,生物辨識解鎖無法使用。" }, "authenticating": { - "message": "Authenticating" + "message": "驗證中" }, "fillGeneratedPassword": { - "message": "Fill generated password", + "message": "自動填入產生的密碼", "description": "Heading for the password generator within the inline menu" }, "passwordRegenerated": { - "message": "Password regenerated", + "message": "密碼已重新產生", "description": "Notification message for when a password has been regenerated" }, "saveLoginToBitwarden": { - "message": "Save login to Bitwarden?", + "message": "在 Bitwarden 中儲存登入資訊?", "description": "Confirmation message for saving a login to Bitwarden" }, "spaceCharacterDescriptor": { - "message": "Space", + "message": "空格", "description": "Represents the space key in screen reader content as a readable word" }, "tildeCharacterDescriptor": { - "message": "Tilde", + "message": "波浪", "description": "Represents the ~ key in screen reader content as a readable word" }, "backtickCharacterDescriptor": { - "message": "Backtick", + "message": "重音符", "description": "Represents the ` key in screen reader content as a readable word" }, "exclamationCharacterDescriptor": { - "message": "Exclamation mark", + "message": "驚歎號", "description": "Represents the ! key in screen reader content as a readable word" }, "atSignCharacterDescriptor": { - "message": "At sign", + "message": "在符號", "description": "Represents the @ key in screen reader content as a readable word" }, "hashSignCharacterDescriptor": { - "message": "Hash sign", + "message": "井字號", "description": "Represents the # key in screen reader content as a readable word" }, "dollarSignCharacterDescriptor": { - "message": "Dollar sign", + "message": "錢字號", "description": "Represents the $ key in screen reader content as a readable word" }, "percentSignCharacterDescriptor": { - "message": "Percent sign", + "message": "百分比", "description": "Represents the % key in screen reader content as a readable word" }, "caretCharacterDescriptor": { - "message": "Caret", + "message": "插入號", "description": "Represents the ^ key in screen reader content as a readable word" }, "ampersandCharacterDescriptor": { - "message": "Ampersand", + "message": "和符號", "description": "Represents the & key in screen reader content as a readable word" }, "asteriskCharacterDescriptor": { - "message": "Asterisk", + "message": "星號", "description": "Represents the * key in screen reader content as a readable word" }, "parenLeftCharacterDescriptor": { - "message": "Left parenthesis", + "message": "左括號", "description": "Represents the ( key in screen reader content as a readable word" }, "parenRightCharacterDescriptor": { - "message": "Right parenthesis", + "message": "右括號", "description": "Represents the ) key in screen reader content as a readable word" }, "hyphenCharacterDescriptor": { - "message": "Underscore", + "message": "底線", "description": "Represents the _ key in screen reader content as a readable word" }, "underscoreCharacterDescriptor": { - "message": "Hyphen", + "message": "連字號", "description": "Represents the - key in screen reader content as a readable word" }, "plusCharacterDescriptor": { - "message": "Plus", + "message": "加號", "description": "Represents the + key in screen reader content as a readable word" }, "equalsCharacterDescriptor": { - "message": "Equals", + "message": "等號", "description": "Represents the = key in screen reader content as a readable word" }, "braceLeftCharacterDescriptor": { - "message": "Left brace", + "message": "左大括號", "description": "Represents the { key in screen reader content as a readable word" }, "braceRightCharacterDescriptor": { - "message": "Right brace", + "message": "右大括號", "description": "Represents the } key in screen reader content as a readable word" }, "bracketLeftCharacterDescriptor": { - "message": "Left bracket", + "message": "左中括號", "description": "Represents the [ key in screen reader content as a readable word" }, "bracketRightCharacterDescriptor": { - "message": "Right bracket", + "message": "右中括號", "description": "Represents the ] key in screen reader content as a readable word" }, "pipeCharacterDescriptor": { - "message": "Pipe", + "message": "垂直符號", "description": "Represents the | key in screen reader content as a readable word" }, "backSlashCharacterDescriptor": { - "message": "Back slash", + "message": "反斜線", "description": "Represents the back slash key in screen reader content as a readable word" }, "colonCharacterDescriptor": { - "message": "Colon", + "message": "冒號", "description": "Represents the : key in screen reader content as a readable word" }, "semicolonCharacterDescriptor": { - "message": "Semicolon", + "message": "分號", "description": "Represents the ; key in screen reader content as a readable word" }, "doubleQuoteCharacterDescriptor": { - "message": "Double quote", + "message": "雙引號", "description": "Represents the double quote key in screen reader content as a readable word" }, "singleQuoteCharacterDescriptor": { - "message": "Single quote", + "message": "單引號", "description": "Represents the ' key in screen reader content as a readable word" }, "lessThanCharacterDescriptor": { - "message": "Less than", + "message": "小於", "description": "Represents the < key in screen reader content as a readable word" }, "greaterThanCharacterDescriptor": { - "message": "Greater than", + "message": "大於", "description": "Represents the > key in screen reader content as a readable word" }, "commaCharacterDescriptor": { - "message": "Comma", + "message": "逗號", "description": "Represents the , key in screen reader content as a readable word" }, "periodCharacterDescriptor": { - "message": "Period", + "message": "句號", "description": "Represents the . key in screen reader content as a readable word" }, "questionCharacterDescriptor": { - "message": "Question mark", + "message": "問號", "description": "Represents the ? key in screen reader content as a readable word" }, "forwardSlashCharacterDescriptor": { - "message": "Forward slash", + "message": "斜線", "description": "Represents the / key in screen reader content as a readable word" }, "lowercaseAriaLabel": { - "message": "Lowercase" + "message": "小寫" }, "uppercaseAriaLabel": { - "message": "Uppercase" + "message": "大寫" }, "generatedPassword": { - "message": "Generated password" + "message": "已產生的密碼" }, "compactMode": { - "message": "Compact mode" + "message": "緊湊模式" }, "beta": { - "message": "Beta" + "message": "測試版" }, "importantNotice": { - "message": "Important notice" + "message": "重要通知" }, "setupTwoStepLogin": { - "message": "Set up two-step login" + "message": "啟動兩階段登入" }, "newDeviceVerificationNoticeContentPage1": { - "message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025." + "message": "從 2025 年 2 月開始,Bitwarden 會傳送代碼到您的帳號電子郵件中來驗證新裝置的登入。" }, "newDeviceVerificationNoticeContentPage2": { - "message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access." + "message": "您可以啟動兩階段認證來保護您的帳號或更改您可以存取的電子郵件位址。" }, "remindMeLater": { - "message": "Remind me later" + "message": "稍後再提醒我" }, "newDeviceVerificationNoticePageOneFormContent": { - "message": "Do you have reliable access to your email, $EMAIL$?", + "message": "您可以存取您的電子郵件位址 $EMAIL$ 嗎?", "placeholders": { "email": { "content": "$1", @@ -4828,24 +4884,24 @@ } }, "newDeviceVerificationNoticePageOneEmailAccessNo": { - "message": "No, I do not" + "message": "不,我不行" }, "newDeviceVerificationNoticePageOneEmailAccessYes": { - "message": "Yes, I can reliably access my email" + "message": "是,我可以存取我的電子郵件位址" }, "turnOnTwoStepLogin": { - "message": "Turn on two-step login" + "message": "啟動兩階段登入" }, "changeAcctEmail": { - "message": "Change account email" + "message": "更改帳號電子郵件位址" }, "extensionWidth": { - "message": "Extension width" + "message": "擴充套件寬度" }, "wide": { - "message": "Wide" + "message": "寬度" }, "extraWide": { - "message": "Extra wide" + "message": "更寬" } } diff --git a/apps/browser/src/auth/popup/account-switching/account.component.ts b/apps/browser/src/auth/popup/account-switching/account.component.ts index 104241e9c7b..dad74977d34 100644 --- a/apps/browser/src/auth/popup/account-switching/account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/account.component.ts @@ -8,6 +8,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AvatarModule, ItemModule } from "@bitwarden/components"; +import { BiometricsService } from "@bitwarden/key-management"; import { AccountSwitcherService, AvailableAccount } from "./services/account-switcher.service"; @@ -26,6 +27,7 @@ export class AccountComponent { private location: Location, private i18nService: I18nService, private logService: LogService, + private biometricsService: BiometricsService, ) {} get specialAccountAddId() { @@ -45,6 +47,9 @@ export class AccountComponent { // locked or logged out account statuses are handled by background and app.component if (result?.status === AuthenticationStatus.Unlocked) { this.location.back(); + await this.biometricsService.setShouldAutopromptNow(false); + } else { + await this.biometricsService.setShouldAutopromptNow(true); } this.loading.emit(false); } diff --git a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts index ad7e6f67361..841eefda0ad 100644 --- a/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts +++ b/apps/browser/src/auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.stories.ts @@ -25,6 +25,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, I18nMockService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RegistrationCheckEmailIcon } from "../../../../../../libs/auth/src/angular/icons/registration-check-email.icon"; import { PopupRouterCacheService } from "../../../platform/popup/view-cache/popup-router-cache.service"; import { AccountSwitcherService } from "../account-switching/services/account-switcher.service"; diff --git a/apps/browser/src/auth/popup/settings/account-security.component.html b/apps/browser/src/auth/popup/settings/account-security.component.html index e0dfde7be77..0f2754b2bf2 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.html +++ b/apps/browser/src/auth/popup/settings/account-security.component.html @@ -11,13 +11,16 @@

{{ "unlockMethods" | i18n }}

- + {{ "unlockWithBiometrics" | i18n }} + + {{ biometricUnavailabilityReason }} + - + {{ "otherOptions" | i18n }} - - - {{ "lockNow" | i18n }} - - - - {{ "logOut" | i18n }} - diff --git a/apps/browser/src/auth/popup/settings/account-security.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts index 86eea889fdd..7e094fe508b 100644 --- a/apps/browser/src/auth/popup/settings/account-security.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -17,6 +17,7 @@ import { Subject, switchMap, takeUntil, + timer, } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -53,11 +54,15 @@ import { TypographyModule, ToastService, } from "@bitwarden/components"; -import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management"; +import { + KeyService, + BiometricsService, + BiometricStateService, + BiometricsStatus, +} from "@bitwarden/key-management"; import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component"; @@ -99,9 +104,8 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { availableVaultTimeoutActions: VaultTimeoutAction[] = []; vaultTimeoutOptions: VaultTimeoutOption[] = []; hasVaultTimeoutPolicy = false; - supportsBiometric: boolean; + biometricUnavailabilityReason: string; showChangeMasterPass = true; - accountSwitcherEnabled = false; form = this.formBuilder.group({ vaultTimeout: [null as VaultTimeout | null], @@ -134,9 +138,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private toastService: ToastService, private biometricsService: BiometricsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } + ) {} async ngOnInit() { const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); @@ -199,7 +201,41 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { }; this.form.patchValue(initialValues, { emitEvent: false }); - this.supportsBiometric = await this.biometricsService.supportsBiometric(); + timer(0, 1000) + .pipe( + switchMap(async () => { + const status = await this.biometricsService.getBiometricsStatusForUser(activeAccount.id); + const biometricSettingAvailable = + !(await BrowserApi.permissionsGranted(["nativeMessaging"])) || + (status !== BiometricsStatus.DesktopDisconnected && + status !== BiometricsStatus.NotEnabledInConnectedDesktopApp) || + (await this.vaultTimeoutSettingsService.isBiometricLockSet()); + if (!biometricSettingAvailable) { + this.form.controls.biometric.disable({ emitEvent: false }); + } else { + this.form.controls.biometric.enable({ emitEvent: false }); + } + + if (status === BiometricsStatus.DesktopDisconnected && !biometricSettingAvailable) { + this.biometricUnavailabilityReason = this.i18nService.t( + "biometricsStatusHelptextDesktopDisconnected", + ); + } else if ( + status === BiometricsStatus.NotEnabledInConnectedDesktopApp && + !biometricSettingAvailable + ) { + this.biometricUnavailabilityReason = this.i18nService.t( + "biometricsStatusHelptextNotEnabledInDesktop", + activeAccount.email, + ); + } else { + this.biometricUnavailabilityReason = ""; + } + }), + takeUntil(this.destroy$), + ) + .subscribe(); + this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword(); this.form.controls.vaultTimeout.valueChanges @@ -399,7 +435,7 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { } async updateBiometric(enabled: boolean) { - if (enabled && this.supportsBiometric) { + if (enabled) { let granted; try { granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] }); @@ -471,7 +507,18 @@ export class AccountSecurityComponent implements OnInit, OnDestroy { const biometricsPromise = async () => { try { - const result = await this.biometricsService.authenticateBiometric(); + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + let result = false; + try { + const userKey = await this.biometricsService.unlockWithBiometricsForUser(userId); + result = await this.keyService.validateUserKey(userKey, userId); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + result = false; + } // prevent duplicate dialog biometricsResponseReceived = true; diff --git a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts index 0917b2703cf..53aedc7a5f3 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-duo.component.ts @@ -13,11 +13,23 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import { ZonedMessageListenerService } from "../../platform/browser/zoned-message-listener.service"; diff --git a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts index b6211bba05f..723152adfab 100644 --- a/apps/browser/src/auth/popup/two-factor-auth-email.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth-email.component.ts @@ -6,12 +6,26 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms"; import { TwoFactorAuthEmailComponent as TwoFactorAuthEmailBaseComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-email.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AsyncActionsModule } from "../../../../../libs/components/src/async-actions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../../libs/components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DialogService } from "../../../../../libs/components/src/dialog"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { FormFieldModule } from "../../../../../libs/components/src/form-field"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LinkModule } from "../../../../../libs/components/src/link"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nPipe } from "../../../../../libs/components/src/shared/i18n.pipe"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../../libs/components/src/typography"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts index 3cb82118597..f22bbbe202c 100644 --- a/apps/browser/src/auth/popup/two-factor-auth.component.ts +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -37,6 +37,8 @@ import { ToastService, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, diff --git a/apps/browser/src/autofill/background/abstractions/overlay.background.ts b/apps/browser/src/autofill/background/abstractions/overlay.background.ts index 03284f3fd89..6ad9b8e06fd 100644 --- a/apps/browser/src/autofill/background/abstractions/overlay.background.ts +++ b/apps/browser/src/autofill/background/abstractions/overlay.background.ts @@ -57,6 +57,17 @@ export type InlineMenuElementPosition = { height: number; }; +export type FieldRect = { + bottom: number; + height: number; + left: number; + right: number; + top: number; + width: number; + x: number; + y: number; +}; + export type InlineMenuPosition = { button?: InlineMenuElementPosition; list?: InlineMenuElementPosition; @@ -134,6 +145,7 @@ export type OverlayBackgroundExtensionMessage = { isFieldCurrentlyFilling?: boolean; subFrameData?: SubFrameOffsetData; focusedFieldData?: FocusedFieldData; + allFieldsRect?: any; isOpeningFullInlineMenu?: boolean; styles?: Partial; data?: LockedVaultPendingNotificationsData; diff --git a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts index 73f936bb591..a300ac08660 100644 --- a/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts +++ b/apps/browser/src/autofill/background/auto-submit-login.background.spec.ts @@ -453,12 +453,16 @@ describe("AutoSubmitLoginBackground", () => { sendMockExtensionMessage({ command: "triggerAutoSubmitLogin" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); it("skips acting on messages whose command does not have a registered handler", () => { sendMockExtensionMessage({ command: "someInvalidCommand" }, sender); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(autofillService.doAutoFillOnTab).not.toHaveBeenCalled; }); diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index 512a9ff4c2a..c3a6357ed05 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -1923,7 +1923,17 @@ describe("OverlayBackground", () => { it("returns true if the overlay login ciphers are populated", async () => { overlayBackground["inlineMenuCiphers"] = new Map([ - ["inline-menu-cipher-0", mock({ type: CipherType.Login })], + [ + "inline-menu-cipher-0", + mock({ + type: CipherType.Login, + login: { + username: "username1", + password: "password1", + uri: "https://example.com", + }, + }), + ], ]); await overlayBackground["getInlineMenuCipherData"](); @@ -2903,6 +2913,124 @@ describe("OverlayBackground", () => { ); }); }); + describe("handles menu position when input is focused", () => { + it("sets button and menu width and position when non-multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + + expect(menuPostion).toEqual({ + width: "49px", + top: "366px", + left: "1271px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "317px", + left: "1271px", + }); + }); + it("sets button and menu width and position when multi-input totp field is focused", async () => { + const subframe = { + top: 0, + left: 0, + url: "", + frameId: 0, + }; + + const totpFields = [ + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__0" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__1" }), + createAutofillFieldMock({ autoCompleteType: "one-time-code", opid: "__2" }), + ]; + const allFieldData = [ + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__0", + rect: { + x: 1041.5, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1090.828125, + bottom: 366.171875, + left: 1041.5, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__1", + rect: { + x: 1098.828125, + y: 302.171875, + width: 49.328125, + height: 64, + top: 302.171875, + right: 1148.15625, + bottom: 366.171875, + left: 1098.828125, + }, + }), + createAutofillFieldMock({ + autoCompleteType: "one-time-code", + opid: "__2", + rect: { + x: 1156.15625, + y: 302.171875, + width: 249.328125, + height: 64, + top: 302.171875, + right: 2205.484375, + bottom: 366.171875, + left: 2156.15625, + }, + }), + ]; + overlayBackground["focusedFieldData"] = createFocusedFieldDataMock({ + focusedFieldRects: { + width: 49.328125, + height: 64, + top: 302.171875, + left: 1270.8125, + }, + }); + + overlayBackground["allFieldData"] = allFieldData; + jest.spyOn(overlayBackground as any, "isTotpFieldForCurrentField").mockReturnValue(true); + jest.spyOn(overlayBackground as any, "getTotpFields").mockReturnValue(totpFields); + + const buttonPostion = overlayBackground["getInlineMenuButtonPosition"](subframe); + const menuPostion = overlayBackground["getInlineMenuListPosition"](subframe); + expect(menuPostion).toEqual({ + width: "1164px", + top: "366px", + left: "1042px", + }); + expect(buttonPostion).toEqual({ + width: "34px", + height: "34px", + top: "292px", + left: "2187px", + }); + }); + }); describe("triggerDelayedAutofillInlineMenuClosure message handler", () => { it("skips triggering the delayed closure of the inline menu if a field is currently focused", async () => { diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 8b577ccccf5..3d2b1ec783c 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -66,9 +66,11 @@ import { InlineMenuFormFieldData } from "../services/abstractions/autofill-overl import { AutofillService, PageDetail } from "../services/abstractions/autofill.service"; import { InlineMenuFieldQualificationService } from "../services/abstractions/inline-menu-field-qualifications.service"; import { + areKeyValuesNull, generateDomainMatchPatterns, generateRandomChars, isInvalidResponseStatusCode, + rectHasSize, specialCharacterToKeyMap, } from "../utils"; @@ -129,6 +131,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private currentInlineMenuCiphersCount: number = 0; private currentAddNewItemData: CurrentAddNewItemData; private focusedFieldData: FocusedFieldData; + private allFieldData: AutofillField[]; private isFieldCurrentlyFocused: boolean = false; private isFieldCurrentlyFilling: boolean = false; private isInlineMenuButtonVisible: boolean = false; @@ -556,6 +559,28 @@ export class OverlayBackground implements OverlayBackgroundInterface { for (let cipherIndex = 0; cipherIndex < inlineMenuCiphersArray.length; cipherIndex++) { const [inlineMenuCipherId, cipher] = inlineMenuCiphersArray[cipherIndex]; + + switch (cipher.type) { + case CipherType.Card: + if (areKeyValuesNull(cipher.card)) { + continue; + } + break; + + case CipherType.Identity: + if (areKeyValuesNull(cipher.identity)) { + continue; + } + break; + + case CipherType.Login: + if ( + areKeyValuesNull(cipher.login, ["username", "password", "totp", "fido2Credentials"]) + ) { + continue; + } + break; + } if (!this.focusedFieldMatchesFillType(cipher.type)) { continue; } @@ -1344,6 +1369,71 @@ export class OverlayBackground implements OverlayBackgroundInterface { this.isInlineMenuListVisible = false; } + /** + * Get all the totp fields for the tab and frame of the currently focused field + */ + private getTotpFields(): AutofillField[] { + const currentTabId = this.focusedFieldData?.tabId; + const currentFrameId = this.focusedFieldData?.frameId; + const pageDetailsMap = this.pageDetailsForTab[currentTabId]; + const pageDetails = pageDetailsMap?.get(currentFrameId); + + const fields = pageDetails.details.fields; + const totpFields = fields.filter((f) => + this.inlineMenuFieldQualificationService.isTotpField(f), + ); + + return totpFields; + } + + /** + * calculates the postion and width for multi-input totp field inline menu + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputMenuBounds(totpFieldArray: AutofillField[]) { + // Filter the fields based on the provided totpfields + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + // Return null if no matching objects are found + if (filteredObjects.length === 0) { + return null; + } + // Calculate the smallest left and largest right values to determine width + const left = Math.min( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.left), + ); + const largestRight = Math.max( + ...filteredObjects.filter((obj) => rectHasSize(obj.rect)).map((obj) => obj.rect.right), + ); + + const width = largestRight - left; + + return { left, width }; + } + + /** + * calculates the postion for multi-input totp field inline button + * @param totpFieldArray - the totp fields used to evaluate the position of the menu + */ + private calculateTotpMultiInputButtonBounds(totpFieldArray: AutofillField[]) { + const filteredObjects = this.allFieldData.filter((obj) => + totpFieldArray.some((o) => o.opid === obj.opid), + ); + + if (filteredObjects.length === 0) { + return null; + } + + const maxRight = Math.max(...filteredObjects.map((obj) => obj.rect.right)); + const maxObject = filteredObjects.find((obj) => obj.rect.right === maxRight); + const top = maxObject.rect.top - maxObject.rect.height * 0.39; + const left = maxRight - maxObject.rect.height * 0.3; + + return { left, top }; + } + /** * Updates the position of either the inline menu list or button. The position * is based on the focused field's position and dimensions. @@ -1449,8 +1539,17 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { width, height } = this.focusedFieldData.focusedFieldRects; + let { top, left } = this.focusedFieldData.focusedFieldRects; const { paddingRight, paddingLeft } = this.focusedFieldData.focusedFieldStyles; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + if (totpFields.length > 1) { + ({ left, top } = this.calculateTotpMultiInputButtonBounds(totpFields)); + } + } + let elementOffset = height * 0.37; if (height >= 35) { elementOffset = height >= 50 ? height * 0.47 : height * 0.42; @@ -1489,7 +1588,16 @@ export class OverlayBackground implements OverlayBackgroundInterface { const subFrameTopOffset = subFrameOffsets?.top || 0; const subFrameLeftOffset = subFrameOffsets?.left || 0; - const { top, left, width, height } = this.focusedFieldData.focusedFieldRects; + const { top, height } = this.focusedFieldData.focusedFieldRects; + let { left, width } = this.focusedFieldData.focusedFieldRects; + + if (this.isTotpFieldForCurrentField()) { + const totpFields = this.getTotpFields(); + + if (totpFields.length > 1) { + ({ left, width } = this.calculateTotpMultiInputMenuBounds(totpFields)); + } + } this.inlineMenuPosition.list = { top: Math.round(top + height + subFrameTopOffset), @@ -1512,7 +1620,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { * @param sender - The sender of the extension message */ private setFocusedFieldData( - { focusedFieldData }: OverlayBackgroundExtensionMessage, + { focusedFieldData, allFieldsRect }: OverlayBackgroundExtensionMessage, sender: chrome.runtime.MessageSender, ) { if ( @@ -1529,6 +1637,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { const previousFocusedFieldData = this.focusedFieldData; this.focusedFieldData = { ...focusedFieldData, tabId: sender.tab.id, frameId: sender.frameId }; + this.allFieldData = allFieldsRect; this.isFieldCurrentlyFocused = true; if (this.shouldUpdatePasswordGeneratorMenuOnFieldFocus()) { diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 21eadfaf668..79998b65205 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -1,12 +1,14 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { NOOP_COMMAND_SUFFIX } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -19,6 +21,7 @@ describe("context-menu", () => { let i18nService: MockProxy; let logService: MockProxy; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; let removeAllSpy: jest.SpyInstance void]>; let createSpy: jest.SpyInstance< @@ -34,6 +37,7 @@ describe("context-menu", () => { i18nService = mock(); logService = mock(); billingAccountProfileStateService = mock(); + accountService = mock(); removeAllSpy = jest .spyOn(chrome.contextMenus, "removeAll") @@ -53,8 +57,15 @@ describe("context-menu", () => { i18nService, logService, billingAccountProfileStateService, + accountService, ); autofillSettingsService.enableContextMenu$ = of(true); + accountService.activeAccount$ = of({ + id: "userId" as UserId, + email: "", + emailVerified: false, + name: undefined, + }); }); afterEach(() => jest.resetAllMocks()); @@ -69,7 +80,7 @@ describe("context-menu", () => { }); it("has menu enabled, but does not have premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -77,7 +88,7 @@ describe("context-menu", () => { }); it("has menu enabled and has premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); const createdMenu = await sut.init(); expect(createdMenu).toBeTruthy(); @@ -131,16 +142,15 @@ describe("context-menu", () => { }); it("create entry for each cipher piece", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await sut.loadOptions("TEST_TITLE", "1", createCipher()); - // One for autofill, copy username, copy password, and copy totp code expect(createSpy).toHaveBeenCalledTimes(4); }); it("creates a login/unlock item for each context menu action option when user is not authenticated", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await sut.loadOptions("TEST_TITLE", "NOOP"); diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index e755524da47..41d88439e8f 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -149,6 +150,7 @@ export class MainContextMenuHandler { private i18nService: I18nService, private logService: LogService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} /** @@ -168,11 +170,13 @@ export class MainContextMenuHandler { this.initRunning = true; try { + const account = await firstValueFrom(this.accountService.activeAccount$); + const hasPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ); + for (const options of this.initContextMenuItems) { - if ( - options.checkPremiumAccess && - !(await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$)) - ) { + if (options.checkPremiumAccess && !hasPremium) { continue; } @@ -267,8 +271,9 @@ export class MainContextMenuHandler { await createChildItem(COPY_USERNAME_ID); } + const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); if (canAccessPremium && (!cipher || !Utils.isNullOrEmpty(cipher.login?.totp))) { await createChildItem(COPY_VERIFICATION_CODE_ID); diff --git a/apps/browser/src/autofill/content/auto-submit-login.spec.ts b/apps/browser/src/autofill/content/auto-submit-login.spec.ts index ff1dbd4e945..d70fc1e7446 100644 --- a/apps/browser/src/autofill/content/auto-submit-login.spec.ts +++ b/apps/browser/src/autofill/content/auto-submit-login.spec.ts @@ -46,6 +46,8 @@ describe("AutoSubmitLogin content script", () => { beforeEach(() => { jest.useFakeTimers(); setupEnvironmentDefaults(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./auto-submit-login"); }); diff --git a/apps/browser/src/autofill/content/components/buttons/action-button.ts b/apps/browser/src/autofill/content/components/buttons/action-button.ts new file mode 100644 index 00000000000..a9b4742b448 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/action-button.ts @@ -0,0 +1,66 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "../constants/styles"; + +export function ActionButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + const handleButtonClick = (event: Event) => { + if (!disabled) { + buttonAction(event); + } + }; + + return html` + + `; +} + +const actionButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.body2} + + user-select: none; + border: 1px solid transparent; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["3"]}; + width: 100%; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + font-weight: 700; + + ${disabled + ? ` + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + background-color: ${themes[theme].primary["600"]}; + cursor: pointer; + color: ${themes[theme].text.contrast}; + + :hover { + border-color: ${themes[theme].primary["700"]}; + background-color: ${themes[theme].primary["700"]}; + color: ${themes[theme].text.contrast}; + } + `} +`; diff --git a/apps/browser/src/autofill/content/components/buttons/badge-button.ts b/apps/browser/src/autofill/content/components/buttons/badge-button.ts new file mode 100644 index 00000000000..3b3b84f8166 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/badge-button.ts @@ -0,0 +1,67 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "../constants/styles"; + +export function BadgeButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + const handleButtonClick = (event: Event) => { + if (!disabled) { + buttonAction(event); + } + }; + + return html` + + `; +} + +const badgeButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.helperMedium} + + user-select: none; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["2"]}; + max-height: fit-content; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + font-weight: 500; + + ${disabled + ? ` + border: 0.5px solid ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + border: 0.5px solid ${themes[theme].primary["700"]}; + background-color: ${themes[theme].primary["100"]}; + cursor: pointer; + color: ${themes[theme].primary["700"]}; + + :hover { + border-color: ${themes[theme].primary["600"]}; + background-color: ${themes[theme].primary["600"]}; + color: ${themes[theme].text.contrast}; + } + `} +`; diff --git a/apps/browser/src/autofill/content/components/buttons/close-button.ts b/apps/browser/src/autofill/content/components/buttons/close-button.ts new file mode 100644 index 00000000000..c32d0c130e3 --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/close-button.ts @@ -0,0 +1,39 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { spacing, themes } from "../constants/styles"; +import { Close as CloseIcon } from "../icons"; + +export function CloseButton({ + handleCloseNotification, + theme, +}: { + handleCloseNotification: (e: Event) => void; + theme: Theme; +}) { + return html` + + `; +} + +const closeButtonStyles = (theme: Theme) => css` + border: 1px solid transparent; + border-radius: ${spacing["1"]}; + background-color: transparent; + cursor: pointer; + width: 36px; + height: 36px; + + :hover { + border: 1px solid ${themes[theme].primary["600"]}; + } + + > svg { + width: 20px; + height: 20px; + } +`; diff --git a/apps/browser/src/autofill/content/components/buttons/edit-button.ts b/apps/browser/src/autofill/content/components/buttons/edit-button.ts new file mode 100644 index 00000000000..cacd2b59f0e --- /dev/null +++ b/apps/browser/src/autofill/content/components/buttons/edit-button.ts @@ -0,0 +1,62 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography, spacing } from "../constants/styles"; +import { PencilSquare } from "../icons"; + +export function EditButton({ + buttonAction, + buttonText, + disabled = false, + theme, +}: { + buttonAction: (e: Event) => void; + buttonText: string; + disabled?: boolean; + theme: Theme; +}) { + return html` + + `; +} + +const editButtonStyles = ({ disabled, theme }: { disabled?: boolean; theme: Theme }) => css` + ${typography.helperMedium} + + user-select: none; + display: flex; + border: 1px solid transparent; + border-radius: ${spacing["1"]}; + background-color: transparent; + padding: ${spacing["1"]}; + max-height: fit-content; + overflow: hidden; + + ${!disabled + ? ` + cursor: pointer; + + :hover { + border-color: ${themes[theme].primary["600"]}; + } + ` + : ""} + + > svg { + width: 16px; + height: fit-content; + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-action.ts b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts new file mode 100644 index 00000000000..2d386d34d6a --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-action.ts @@ -0,0 +1,31 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + +import { BadgeButton } from "../../../content/components/buttons/badge-button"; +import { EditButton } from "../../../content/components/buttons/edit-button"; +import { NotificationTypes } from "../../../notification/abstractions/notification-bar"; + +export function CipherAction({ + handleAction = () => { + /* no-op */ + }, + notificationType, + theme, +}: { + handleAction?: (e: Event) => void; + notificationType: typeof NotificationTypes.Change | typeof NotificationTypes.Add; + theme: Theme; +}) { + return notificationType === NotificationTypes.Change + ? BadgeButton({ + buttonAction: handleAction, + // @TODO localize + buttonText: "Update item", + theme, + }) + : EditButton({ + buttonAction: handleAction, + // @TODO localize + buttonText: "Edit item", + theme, + }); +} diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts b/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts new file mode 100644 index 00000000000..73d3f7604a9 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-icon.ts @@ -0,0 +1,33 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { Globe } from "../../../content/components/icons"; + +/** + * @param {string} props.color contextual color override if no icon URI is available + * @param {string} props.size valid CSS `width` value, represents the width-basis of the graphic, with height maintaining original aspect-ratio + */ +export function CipherIcon({ + color, + size, + theme, + uri, +}: { + color: string; + size: string; + theme: Theme; + uri?: string; +}) { + const iconClass = cipherIconStyle({ width: size }); + + return uri + ? html`` + : html`${Globe({ color, theme })}`; +} + +const cipherIconStyle = ({ width }: { width: string }) => css` + width: ${width}; + height: fit-content; +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts new file mode 100644 index 00000000000..38b4292f8e5 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-indicator-icons.ts @@ -0,0 +1,35 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes } from "../../../content/components/constants/styles"; +import { Business, Family } from "../../../content/components/icons"; + +// @TODO connect data source to icon checks +// @TODO support other indicator types (attachments, etc) +export function CipherInfoIndicatorIcons({ + isBusinessOrg, + isFamilyOrg, + theme, +}: { + isBusinessOrg?: boolean; + isFamilyOrg?: boolean; + theme: Theme; +}) { + const indicatorIcons = [ + ...(isBusinessOrg ? [Business({ color: themes[theme].text.muted, theme })] : []), + ...(isFamilyOrg ? [Family({ color: themes[theme].text.muted, theme })] : []), + ]; + + return indicatorIcons.length + ? html` ${indicatorIcons} ` + : null; // @TODO null case should be handled by parent +} + +const cipherInfoIndicatorIconsStyles = css` + > svg { + width: fit-content; + height: 12px; + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts new file mode 100644 index 00000000000..de374b44a97 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts @@ -0,0 +1,48 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes, typography } from "../../../content/components/constants/styles"; + +import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons"; +import { CipherData } from "./types"; + +// @TODO support other cipher types (card, identity, notes, etc) +export function CipherInfo({ cipher, theme }: { cipher: CipherData; theme: Theme }) { + const { name, login } = cipher; + + return html` +
+ + ${[name, CipherInfoIndicatorIcons({ theme })]} + + + ${login?.username + ? html`${login.username}` + : null} +
+ `; +} + +const cipherInfoPrimaryTextStyles = (theme: Theme) => css` + ${typography.body2} + + gap: 2px; + display: flex; + display: block; + overflow-x: hidden; + text-overflow: ellipsis; + color: ${themes[theme].text.main}; + font-weight: 500; +`; + +const cipherInfoSecondaryTextStyles = (theme: Theme) => css` + ${typography.helperMedium} + + display: block; + overflow-x: hidden; + text-overflow: ellipsis; + color: ${themes[theme].text.muted}; + font-weight: 400; +`; diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts new file mode 100644 index 00000000000..651c20cac3a --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts @@ -0,0 +1,65 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes } from "../../../content/components/constants/styles"; +import { + NotificationType, + NotificationTypes, +} from "../../../notification/abstractions/notification-bar"; + +import { CipherAction } from "./cipher-action"; +import { CipherIcon } from "./cipher-icon"; +import { CipherInfo } from "./cipher-info"; +import { CipherData } from "./types"; + +const cipherIconWidth = "24px"; + +export function CipherItem({ + cipher, + handleAction, + notificationType, + theme = ThemeTypes.Light, +}: { + cipher: CipherData; + handleAction?: (e: Event) => void; + notificationType?: NotificationType; + theme: Theme; +}) { + const { icon } = cipher; + const uri = (icon.imageEnabled && icon.image) || undefined; + + let cipherActionButton = null; + + if (notificationType === NotificationTypes.Change || notificationType === NotificationTypes.Add) { + cipherActionButton = html`
+ ${CipherAction({ handleAction, notificationType, theme })} +
`; + } + + return html` +
+ ${CipherIcon({ color: themes[theme].text.muted, size: cipherIconWidth, theme, uri })} + ${CipherInfo({ theme, cipher })} +
+ ${cipherActionButton} + `; +} + +const cipherItemStyles = css` + gap: ${spacing["2"]}; + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: start; + + > img, + > span { + display: flex; + } + + > div { + max-width: calc(100% - ${cipherIconWidth} - ${spacing["2"]}); + } +`; diff --git a/apps/browser/src/autofill/content/components/cipher/index.ts b/apps/browser/src/autofill/content/components/cipher/index.ts new file mode 100644 index 00000000000..733ddb74b4d --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/index.ts @@ -0,0 +1,5 @@ +export * from "./cipher-action"; +export * from "./cipher-icon"; +export * from "./cipher-indicator-icons"; +export * from "./cipher-info"; +export * from "./cipher-item"; diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts new file mode 100644 index 00000000000..acdee756570 --- /dev/null +++ b/apps/browser/src/autofill/content/components/cipher/types.ts @@ -0,0 +1,48 @@ +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CipherTypes = { + Login: 1, + SecureNote: 2, + Card: 3, + Identity: 4, +} as const; + +type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes]; + +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const CipherRepromptTypes = { + None: 0, + Password: 1, +} as const; + +type CipherRepromptType = (typeof CipherRepromptTypes)[keyof typeof CipherRepromptTypes]; + +export type WebsiteIconData = { + imageEnabled: boolean; + image: string; + fallbackImage: string; + icon: string; +}; + +export type CipherData = { + id: string; + name: string; + type: CipherType; + reprompt: CipherRepromptType; + favorite: boolean; + icon: WebsiteIconData; + accountCreationFieldType?: string; + login?: { + username: string; + passkey: { + rpName: string; + userName: string; + } | null; + }; + card?: string; + identity?: { + fullName: string; + username?: string; + }; +}; diff --git a/apps/browser/src/autofill/content/components/constants/styles.ts b/apps/browser/src/autofill/content/components/constants/styles.ts new file mode 100644 index 00000000000..cd6054e90ba --- /dev/null +++ b/apps/browser/src/autofill/content/components/constants/styles.ts @@ -0,0 +1,206 @@ +import { Theme } from "@bitwarden/common/platform/enums"; + +const lightTheme = { + transparent: { + hover: `rgb(0 0 0 / 0.02)`, + }, + shadow: `rgba(168 179 200)`, + primary: { + 100: `rgba(219, 229, 246)`, + 300: `rgba(121, 161, 233)`, + 600: `rgba(23, 93, 220)`, + 700: `rgba(26, 65, 172)`, + }, + secondary: { + 100: `rgba(230, 233, 239)`, + 300: `rgba(168, 179, 200)`, + 500: `rgba(90, 109, 145)`, + 600: `rgba(83, 99, 131)`, + 700: `rgba(63, 75, 99)`, + }, + success: { + 100: `rgba(219, 229, 246)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(26, 65, 172)`, + }, + danger: { + 100: `rgba(255, 236, 239)`, + 600: `rgba(203, 38, 58)`, + 700: `rgba(149, 27, 42)`, + }, + warning: { + 100: `rgba(255, 248, 228)`, + 600: `rgba(255, 191, 0)`, + 700: `rgba(172, 88, 0)`, + }, + info: { + 100: `rgba(219, 229, 246)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(26, 65, 172)`, + }, + art: { + primary: `rgba(2, 15, 102)`, + accent: `rgba(44, 221, 223)`, + }, + text: { + main: `rgba(27, 32, 41)`, + muted: `rgba(90, 109, 145)`, + contrast: `rgba(255, 255, 255)`, + alt2: `rgba(255, 255, 255)`, + code: `rgba(192, 17, 118)`, + }, + background: { + DEFAULT: `rgba(255, 255, 255)`, + alt: `rgba(243, 246, 249)`, + alt2: `rgba(23, 92, 219)`, + alt3: `rgba(26, 65, 172)`, + alt4: `rgba(2, 15, 102)`, + }, + brandLogo: `rgba(23, 93, 220)`, +}; + +const darkTheme = { + transparent: { + hover: `rgb(255 255 255 / 0.02)`, + }, + shadow: `rgba(0, 0, 0)`, + primary: { + 100: `rgba(26, 39, 78)`, + 300: `rgba(26, 65, 172)`, + 600: `rgba(101, 171, 255)`, + 700: `rgba(170, 195, 239)`, + }, + secondary: { + 100: `rgba(48, 57, 70)`, + 300: `rgba(82, 91, 106)`, + 500: `rgba(121, 128, 142)`, + 600: `rgba(143, 152, 166)`, + 700: `rgba(158, 167, 181)`, + }, + success: { + 100: `rgba(11, 111, 21)`, + 600: `rgba(107, 241, 120)`, + 700: `rgba(191, 236, 195)`, + }, + danger: { + 100: `rgba(149, 27, 42)`, + 600: `rgba(255, 78, 99)`, + 700: `rgba(255, 236, 239)`, + }, + warning: { + 100: `rgba(172, 88, 0)`, + 600: `rgba(255, 191, 0)`, + 700: `rgba(255, 248, 228)`, + }, + info: { + 100: `rgba(26, 65, 172)`, + 600: `rgba(121, 161, 233)`, + 700: `rgba(219, 229, 246)`, + }, + art: { + primary: `rgba(243, 246, 249)`, + accent: `rgba(44, 221, 233)`, + }, + text: { + main: `rgba(243, 246, 249)`, + muted: `rgba(136, 152, 181)`, + contrast: `rgba(18 26 39)`, + alt2: `rgba(255, 255, 255)`, + code: `rgba(255, 143, 208)`, + }, + background: { + DEFAULT: `rgba(32, 39, 51)`, + alt: `rgba(18, 26, 39)`, + alt2: `rgba(47, 52, 61)`, + alt3: `rgba(48, 57, 70)`, + alt4: `rgba(18, 26, 39)`, + }, + brandLogo: `rgba(255, 255, 255)`, +}; + +export const themes = { + light: lightTheme, + dark: darkTheme, + + // For compatibility + system: lightTheme, + nord: lightTheme, + solarizedDark: darkTheme, +}; + +export const spacing = { + 4: `16px`, + 3: `12px`, + 2: `8px`, + "1.5": `6px`, + 1: `4px`, +}; + +export const border = { + radius: { + large: `8px`, + full: `9999px`, + }, +}; + +export const typography = { + body1: ` + line-height: 24px; + font-family: "DM Sans", sans-serif; + font-size: 16px; + `, + body2: ` + line-height: 20px; + font-family: "DM Sans", sans-serif; + font-size: 14px; + `, + helperMedium: ` + line-height: 16px; + font-family: "DM Sans", sans-serif; + font-size: 12px; + `, +}; + +export const ruleNames = { + fill: "fill", + stroke: "stroke", +} as const; + +type RuleName = (typeof ruleNames)[keyof typeof ruleNames]; + +/* + * `color` is an intentionally generic name here, since either fill or stroke may apply, due to + * inconsistent SVG construction. This consequently precludes dynamic multi-colored icons here. + */ +export const buildIconColorRule = (color: string, rule: RuleName = ruleNames.fill) => ` + ${rule}: ${color}; +`; + +export function scrollbarStyles(theme: Theme) { + return { + default: ` + /* FireFox & Chrome support */ + scrollbar-color: ${themes[theme].secondary["500"]} ${themes[theme].background.alt}; + `, + safari: ` + /* Safari Support */ + ::-webkit-scrollbar { + overflow: auto; + } + ::-webkit-scrollbar-thumb { + border-width: 4px; + border-style: solid; + border-radius: 0.5rem; + border-color: transparent; + background-clip: content-box; + background-color: ${themes[theme].secondary["500"]}; + } + ::-webkit-scrollbar-track { + ${themes[theme].background.alt}; + } + ::-webkit-scrollbar-thumb:hover { + ${themes[theme].secondary["600"]}; + } + `, + }; +} diff --git a/apps/browser/src/autofill/content/components/dropdown-menu.ts b/apps/browser/src/autofill/content/components/dropdown-menu.ts new file mode 100644 index 00000000000..3e3874b37d7 --- /dev/null +++ b/apps/browser/src/autofill/content/components/dropdown-menu.ts @@ -0,0 +1,121 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { border, themes, typography, spacing } from "./constants/styles"; +import { AngleDown } from "./icons"; + +export function DropdownMenu({ + buttonText, + icon, + disabled = false, + selectAction, + theme, +}: { + selectAction?: (e: Event) => void; + buttonText: string; + icon?: TemplateResult; + disabled?: boolean; + theme: Theme; +}) { + // @TODO placeholder/will not work; make stateful + const showDropdown = false; + const handleButtonClick = (event: Event) => { + // if (!disabled) { + // // show dropdown + // showDropdown = !showDropdown; + // this.requestUpdate(); + // } + }; + + const dropdownMenuItems: TemplateResult[] = []; + + return html` +
+ + ${showDropdown + ? html`
${dropdownMenuItems}
` + : null} +
+ `; +} + +const iconSize = "15px"; + +const dropdownContainerStyles = css` + display: flex; + + > div, + > button { + width: 100%; + } +`; + +const dropdownButtonStyles = ({ disabled, theme }: { disabled: boolean; theme: Theme }) => css` + ${typography.body2} + + font-weight: 400; + gap: ${spacing["1.5"]}; + user-select: none; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; + border-radius: ${border.radius.full}; + padding: ${spacing["1"]} ${spacing["2"]}; + max-height: fit-content; + overflow: hidden; + text-align: center; + text-overflow: ellipsis; + + > svg { + max-width: ${iconSize}; + height: fit-content; + } + + ${disabled + ? ` + border: 1px solid ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].secondary["300"]}; + color: ${themes[theme].text.muted}; + ` + : ` + border: 1px solid ${themes[theme].text.muted}; + background-color: transparent; + cursor: pointer; + color: ${themes[theme].text.muted}; + + :hover { + border-color: ${themes[theme].secondary["700"]}; + background-color: ${themes[theme].secondary["100"]}; + } + `} +`; + +const dropdownButtonTextStyles = css` + max-width: calc(100% - ${iconSize} - ${iconSize}); + overflow-x: hidden; + text-overflow: ellipsis; +`; + +const dropdownMenuStyles = ({ theme }: { theme: Theme }) => css` + color: ${themes[theme].text.main}; + border: 1px solid ${themes[theme].secondary["500"]}; + border-radius: 0.5rem; + background-clip: padding-box; + background-color: ${themes[theme].background.DEFAULT}; + padding: 0.25rem 0.75rem; + position: absolute; + overflow-y: auto; +`; diff --git a/apps/browser/src/autofill/content/components/icons/angle-down.ts b/apps/browser/src/autofill/content/components/icons/angle-down.ts new file mode 100644 index 00000000000..4b85319c18a --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/angle-down.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function AngleDown({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts new file mode 100644 index 00000000000..8df68d79b6e --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/brand-icon-container.ts @@ -0,0 +1,19 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { Shield } from "./shield"; + +export function BrandIconContainer({ iconLink, theme }: { iconLink?: URL; theme: Theme }) { + const Icon = html`
${Shield({ theme })}
`; + + return iconLink ? html`${Icon}` : Icon; +} + +const brandIconContainerStyles = css` + > svg { + width: 20px; + height: fit-content; + } +`; diff --git a/apps/browser/src/autofill/content/components/icons/business.ts b/apps/browser/src/autofill/content/components/icons/business.ts new file mode 100644 index 00000000000..547ee82b547 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/business.ts @@ -0,0 +1,46 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Business({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/close.ts b/apps/browser/src/autofill/content/components/icons/close.ts new file mode 100644 index 00000000000..c94a4b20a6a --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/close.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Close({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts new file mode 100644 index 00000000000..bcc7b3d5432 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/exclamation-triangle.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function ExclamationTriangle({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/family.ts b/apps/browser/src/autofill/content/components/icons/family.ts new file mode 100644 index 00000000000..33e2e422ced --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/family.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Family({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/folder.ts b/apps/browser/src/autofill/content/components/icons/folder.ts new file mode 100644 index 00000000000..7e1f8f197f6 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/folder.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Folder({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/globe.ts b/apps/browser/src/autofill/content/components/icons/globe.ts new file mode 100644 index 00000000000..6697fa93b70 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/globe.ts @@ -0,0 +1,28 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Globe({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/index.ts b/apps/browser/src/autofill/content/components/icons/index.ts new file mode 100644 index 00000000000..992b034afa7 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/index.ts @@ -0,0 +1,12 @@ +export { AngleDown } from "./angle-down"; +export { BrandIconContainer } from "./brand-icon-container"; +export { Business } from "./business"; +export { Close } from "./close"; +export { ExclamationTriangle } from "./exclamation-triangle"; +export { Family } from "./family"; +export { Folder } from "./folder"; +export { Globe } from "./globe"; +export { PartyHorn } from "./party-horn"; +export { PencilSquare } from "./pencil-square"; +export { Shield } from "./shield"; +export { User } from "./user"; diff --git a/apps/browser/src/autofill/content/components/icons/party-horn.ts b/apps/browser/src/autofill/content/components/icons/party-horn.ts new file mode 100644 index 00000000000..dc2144b524f --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/party-horn.ts @@ -0,0 +1,174 @@ +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +// This icon has static multi-colors for each theme +export function PartyHorn({ theme }: { theme: Theme }) { + if (theme === ThemeTypes.Dark) { + return html` + + + + + + + + + + + + + + `; + } + + return html` + + + + + + + + + + + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/pencil-square.ts b/apps/browser/src/autofill/content/components/icons/pencil-square.ts new file mode 100644 index 00000000000..45a8429f883 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/pencil-square.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function PencilSquare({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/shield.ts b/apps/browser/src/autofill/content/components/icons/shield.ts new file mode 100644 index 00000000000..5ffd953e869 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/shield.ts @@ -0,0 +1,19 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function Shield({ color, theme }: { color?: string; theme: Theme }) { + const shapeColor = color || themes[theme].brandLogo; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/icons/user.ts b/apps/browser/src/autofill/content/components/icons/user.ts new file mode 100644 index 00000000000..6babcfa39a9 --- /dev/null +++ b/apps/browser/src/autofill/content/components/icons/user.ts @@ -0,0 +1,27 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { buildIconColorRule, ruleNames, themes } from "../constants/styles"; + +export function User({ + color, + disabled, + theme, +}: { + color?: string; + disabled?: boolean; + theme: Theme; +}) { + const shapeColor = disabled ? themes[theme].secondary["300"] : color || themes[theme].text.main; + + return html` + + + + `; +} diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts new file mode 100644 index 00000000000..6a3ed2e5d1e --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/body.ts @@ -0,0 +1,69 @@ +import createEmotion from "@emotion/css/create-instance"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { NotificationType } from "../../../notification/abstractions/notification-bar"; +import { CipherItem } from "../cipher"; +import { CipherData } from "../cipher/types"; +import { scrollbarStyles, spacing, themes, typography } from "../constants/styles"; +import { ItemRow } from "../rows/item-row"; + +export const componentClassPrefix = "notification-body"; + +const { css } = createEmotion({ + key: componentClassPrefix, +}); + +export function NotificationBody({ + ciphers, + notificationType, + theme = ThemeTypes.Light, +}: { + ciphers: CipherData[]; + customClasses?: string[]; + notificationType?: NotificationType; + theme: Theme; +}) { + // @TODO get client vendor from context + const isSafari = false; + + return html` +
+ ${ciphers.map((cipher) => + ItemRow({ + theme, + children: CipherItem({ + cipher, + notificationType, + theme, + handleAction: () => { + // @TODO connect update or edit actions to handler + }, + }), + }), + )} +
+ `; +} + +const notificationBodyStyles = ({ isSafari, theme }: { isSafari: boolean; theme: Theme }) => css` + ${typography.body1} + + gap: ${spacing["1.5"]}; + display: flex; + flex-flow: column; + background-color: ${themes[theme].background.alt}; + max-height: 123px; + overflow-x: hidden; + overflow-y: auto; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-weight: 400; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + } + + ${isSafari ? scrollbarStyles(theme).safari : scrollbarStyles(theme).default} +`; diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts new file mode 100644 index 00000000000..0cce066cf3a --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/container.ts @@ -0,0 +1,99 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { + NotificationBarIframeInitData, + NotificationTypes, + NotificationType, +} from "../../../notification/abstractions/notification-bar"; +import { createAutofillOverlayCipherDataMock } from "../../../spec/autofill-mocks"; +import { CipherData } from "../cipher/types"; +import { themes, spacing } from "../constants/styles"; + +import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body"; +import { NotificationFooter } from "./footer"; +import { + NotificationHeader, + componentClassPrefix as notificationHeaderClassPrefix, +} from "./header"; + +export function NotificationContainer({ + handleCloseNotification, + i18n, + theme = ThemeTypes.Light, + type, +}: NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void } & { + i18n: { [key: string]: string }; + type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type` +}) { + const headerMessage = getHeaderMessage(i18n, type); + const showBody = true; + + // @TODO remove mock ciphers for development + const ciphers = [ + createAutofillOverlayCipherDataMock(1), + { ...createAutofillOverlayCipherDataMock(2), icon: { imageEnabled: false } }, + { + ...createAutofillOverlayCipherDataMock(3), + icon: { imageEnabled: true, image: "https://localhost:8443/icons/webtests.dev/icon.png" }, + }, + ] as CipherData[]; + + return html` +
+ ${NotificationHeader({ + handleCloseNotification, + standalone: showBody, + message: headerMessage, + theme, + })} + ${showBody + ? NotificationBody({ + ciphers, + notificationType: type, + theme, + }) + : null} + ${NotificationFooter({ + theme, + notificationType: type, + })} +
+ `; +} + +const notificationContainerStyles = (theme: Theme) => css` + position: absolute; + right: 20px; + border: 1px solid ${themes[theme].secondary["300"]}; + border-radius: ${spacing["4"]}; + box-shadow: -2px 4px 6px 0px #0000001a; + background-color: ${themes[theme].background.alt}; + width: 400px; + + [class*="${notificationHeaderClassPrefix}-"] { + border-radius: ${spacing["4"]} ${spacing["4"]} 0 0; + } + + [class*="${notificationBodyClassPrefix}-"] { + margin: ${spacing["3"]} 0 ${spacing["1.5"]} ${spacing["3"]}; + padding-right: ${spacing["3"]}; + } +`; + +function getHeaderMessage(i18n: { [key: string]: string }, type?: NotificationType) { + switch (type) { + case NotificationTypes.Add: + return i18n.saveAsNewLoginAction; + case NotificationTypes.Change: + return i18n.updateLoginPrompt; + case NotificationTypes.Unlock: + return ""; + case NotificationTypes.FilelessImport: + return ""; + default: + return undefined; + } +} diff --git a/apps/browser/src/autofill/content/components/notification/footer.ts b/apps/browser/src/autofill/content/components/notification/footer.ts new file mode 100644 index 00000000000..91a72dc9aab --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/footer.ts @@ -0,0 +1,42 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { + NotificationType, + NotificationTypes, +} from "../../../notification/abstractions/notification-bar"; +import { spacing, themes } from "../constants/styles"; +import { ActionRow } from "../rows/action-row"; +import { ButtonRow } from "../rows/button-row"; + +export function NotificationFooter({ + notificationType, + theme, +}: { + notificationType?: NotificationType; + theme: Theme; +}) { + const isChangeNotification = notificationType === NotificationTypes.Change; + // @TODO localize + const saveNewItemText = "Save as new login"; + + return html` +
+ ${isChangeNotification + ? ActionRow({ itemText: saveNewItemText, handleAction: () => {}, theme }) + : ButtonRow({ theme })} +
+ `; +} + +const notificationFooterStyles = ({ theme }: { theme: Theme }) => css` + display: flex; + background-color: ${themes[theme].background.alt}; + padding: 0 ${spacing[3]} ${spacing[3]} ${spacing[3]}; + + :last-child { + border-radius: 0 0 ${spacing["4"]} ${spacing["4"]}; + } +`; diff --git a/apps/browser/src/autofill/content/components/notification/header-message.ts b/apps/browser/src/autofill/content/components/notification/header-message.ts new file mode 100644 index 00000000000..ccfa58b8970 --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/header-message.ts @@ -0,0 +1,25 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { themes } from "../constants/styles"; + +export function NotificationHeaderMessage({ message, theme }: { message: string; theme: Theme }) { + return html` + ${message} + `; +} + +const notificationHeaderMessageStyles = (theme: Theme) => css` + flex-grow: 1; + overflow-x: hidden; + text-align: left; + text-overflow: ellipsis; + line-height: 28px; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-family: "DM Sans", sans-serif; + font-size: 18px; + font-weight: 600; +`; diff --git a/apps/browser/src/autofill/content/components/notification/header.ts b/apps/browser/src/autofill/content/components/notification/header.ts new file mode 100644 index 00000000000..85f6e48cd5d --- /dev/null +++ b/apps/browser/src/autofill/content/components/notification/header.ts @@ -0,0 +1,61 @@ +import createEmotion from "@emotion/css/create-instance"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { CloseButton } from "../buttons/close-button"; +import { themes } from "../constants/styles"; +import { BrandIconContainer } from "../icons/brand-icon-container"; + +import { NotificationHeaderMessage } from "./header-message"; + +export const componentClassPrefix = "notification-header"; + +const { css } = createEmotion({ + key: componentClassPrefix, +}); + +export function NotificationHeader({ + message, + standalone, + theme = ThemeTypes.Light, + handleCloseNotification, +}: { + message?: string; + standalone: boolean; + theme: Theme; + handleCloseNotification: (e: Event) => void; +}) { + const showIcon = true; + const isDismissable = true; + + return html` +
+ ${showIcon ? BrandIconContainer({ theme }) : null} + ${message ? NotificationHeaderMessage({ message, theme }) : null} + ${isDismissable ? CloseButton({ handleCloseNotification, theme }) : null} +
+ `; +} + +const notificationHeaderStyles = ({ + standalone, + theme, +}: { + standalone: boolean; + theme: Theme; +}) => css` + gap: 8px; + display: flex; + align-items: center; + justify-content: flex-start; + background-color: ${themes[theme].background.alt}; + padding: 12px 16px 8px 16px; + white-space: nowrap; + + ${standalone + ? css` + border-bottom: 0.5px solid ${themes[theme].secondary["300"]}; + ` + : css``} +`; diff --git a/apps/browser/src/autofill/content/components/rows/action-row.ts b/apps/browser/src/autofill/content/components/rows/action-row.ts new file mode 100644 index 00000000000..ad58411baf4 --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/action-row.ts @@ -0,0 +1,53 @@ +import { css } from "@emotion/css"; +import { html } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes, typography } from "../../../content/components/constants/styles"; + +export function ActionRow({ + handleAction, + itemText, + theme = ThemeTypes.Light, +}: { + itemText: string; + handleAction?: (e: Event) => void; + theme: Theme; +}) { + return html` + + `; +} + +const actionRowStyles = (theme: Theme) => css` + ${typography.body2} + + user-select: none; + border-width: 0 0 0.5px 0; + border-style: solid; + border-radius: ${spacing["2"]}; + border-color: ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].background.DEFAULT}; + cursor: pointer; + padding: ${spacing["2"]} ${spacing["3"]}; + width: 100%; + min-height: 40px; + text-align: left; + color: ${themes[theme].primary["600"]}; + font-weight: 700; + + > span { + display: block; + width: calc(100% - 5px); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + :hover { + background-color: ${themes[theme].primary["100"]}; + color: ${themes[theme].primary["600"]}; + } +`; diff --git a/apps/browser/src/autofill/content/components/rows/button-row.ts b/apps/browser/src/autofill/content/components/rows/button-row.ts new file mode 100644 index 00000000000..ce14a242e97 --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/button-row.ts @@ -0,0 +1,73 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme } from "@bitwarden/common/platform/enums"; + +import { ActionButton } from "../../../content/components/buttons/action-button"; +import { spacing, themes } from "../../../content/components/constants/styles"; +import { Folder, User } from "../../../content/components/icons"; +import { DropdownMenu } from "../dropdown-menu"; + +export function ButtonRow({ theme }: { theme: Theme }) { + return html` +
+ ${[ + ActionButton({ + buttonAction: () => {}, + buttonText: "Action Button", + theme, + }), + DropdownContainer({ + children: [ + DropdownMenu({ + buttonText: "You", + icon: User({ color: themes[theme].text.muted, theme }), + theme, + }), + DropdownMenu({ + buttonText: "Folder", + icon: Folder({ color: themes[theme].text.muted, theme }), + disabled: true, + theme, + }), + ], + }), + ]} +
+ `; +} + +function DropdownContainer({ children }: { children: TemplateResult[] }) { + return html`
${children}
`; +} + +const buttonRowStyles = css` + gap: 16px; + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + max-height: 52px; + white-space: nowrap; + + > button { + max-width: min-content; + flex: 1 1 50%; + } + + > div { + flex: 1 1 min-content; + } +`; + +const dropdownContainerStyles = css` + gap: 8px; + display: flex; + align-items: center; + justify-content: flex-end; + overflow: hidden; + + > div { + min-width: calc(50% - ${spacing["1.5"]}); + } +`; diff --git a/apps/browser/src/autofill/content/components/rows/item-row.ts b/apps/browser/src/autofill/content/components/rows/item-row.ts new file mode 100644 index 00000000000..da00fd276ab --- /dev/null +++ b/apps/browser/src/autofill/content/components/rows/item-row.ts @@ -0,0 +1,56 @@ +import { css } from "@emotion/css"; +import { html, TemplateResult } from "lit"; + +import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums"; + +import { spacing, themes, typography } from "../../../content/components/constants/styles"; + +export function ItemRow({ + theme = ThemeTypes.Light, + children, +}: { + theme: Theme; + children: TemplateResult | TemplateResult[]; +}) { + return html`
${children}
`; +} + +export const itemRowStyles = ({ theme }: { theme: Theme }) => css` + ${typography.body1} + + gap: ${spacing["2"]}; + display: flex; + align-items: center; + justify-content: space-between; + border-width: 0 0 0.5px 0; + border-style: solid; + border-radius: ${spacing["2"]}; + border-color: ${themes[theme].secondary["300"]}; + background-color: ${themes[theme].background.DEFAULT}; + padding: ${spacing["2"]} ${spacing["3"]}; + min-height: min-content; + max-height: 52px; + overflow-x: hidden; + white-space: nowrap; + color: ${themes[theme].text.main}; + font-weight: 400; + + > div { + :first-child { + flex: 3 3 75%; + min-width: 25%; + } + + :not(:first-child) { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: flex-end; + max-width: 25%; + + > button { + max-width: min-content; + } + } + } +`; diff --git a/apps/browser/src/autofill/content/content-message-handler.spec.ts b/apps/browser/src/autofill/content/content-message-handler.spec.ts index 226fcb4bd61..a37a2e07678 100644 --- a/apps/browser/src/autofill/content/content-message-handler.spec.ts +++ b/apps/browser/src/autofill/content/content-message-handler.spec.ts @@ -19,6 +19,8 @@ describe("ContentMessageHandler", () => { ); beforeEach(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./content-message-handler"); }); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index b260bfef632..d3e9c29ab58 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ServerConfig } from "../../../../../libs/common/src/platform/abstractions/config/server-config"; +import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config"; + import { AddLoginMessageData, ChangePasswordMessageData, diff --git a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts index 1ad985bc8e9..317f63e756c 100644 --- a/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts +++ b/apps/browser/src/autofill/content/trigger-autofill-script-injection.spec.ts @@ -6,6 +6,8 @@ describe("TriggerAutofillScriptInjection", () => { describe("init", () => { it("sends a message to the extension background", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../content/trigger-autofill-script-injection"); expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts index fde98a58a5f..fd6a79733cb 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/button/bootstrap-autofill-overlay-button.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayButton from "./autofill-overlay-button.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-button.scss"); (function () { diff --git a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts index 714ccbfbee5..5d587bd4293 100644 --- a/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/overlay/pages/list/bootstrap-autofill-overlay-list.deprecated.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import AutofillOverlayList from "./autofill-overlay-list.deprecated"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./legacy-list.scss"); (function () { diff --git a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts index 94bef354a79..8885ed6299c 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-content-script.spec.ts @@ -60,6 +60,8 @@ describe("Fido2 Content Script", () => { chrome.runtime.connect = jest.fn(() => portSpy); it("destroys the messenger when the port is disconnected", () => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); triggerPortOnDisconnectEvent(portSpy); @@ -75,6 +77,8 @@ describe("Fido2 Content Script", () => { const mockResult = { credentialId: "mock" } as CreateCredentialResult; jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const response = await messenger.handler!(message, new AbortController()); @@ -99,6 +103,8 @@ describe("Fido2 Content Script", () => { data: mock(), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, new AbortController()); @@ -121,6 +127,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -141,6 +149,8 @@ describe("Fido2 Content Script", () => { abortController.abort(); }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); await messenger.handler!(message, abortController); @@ -161,6 +171,8 @@ describe("Fido2 Content Script", () => { const abortController = new AbortController(); jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); const result = messenger.handler!(message, abortController); @@ -175,6 +187,8 @@ describe("Fido2 Content Script", () => { contentType: "application/json", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); @@ -193,6 +207,8 @@ describe("Fido2 Content Script", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts index 6b9b41b5aac..69e17d26fe5 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script-append.mv2.spec.ts @@ -22,6 +22,8 @@ describe("FIDO2 page-script for manifest v2", () => { it("skips appending the `page-script.js` file if the document contentType is not `text/html`", () => { Object.defineProperty(window.document, "contentType", { value: "text/plain", writable: true }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).not.toHaveBeenCalled(); @@ -33,6 +35,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); @@ -48,6 +52,8 @@ describe("FIDO2 page-script for manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts index fd033090cd4..4c1761c37ba 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.ts @@ -267,6 +267,8 @@ import { Messenger } from "./messaging/messenger"; clearWaitForFocus(); void messenger.destroy(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /** empty */ } diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts index 31e8c941e86..f1aec69193b 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-supported.spec.ts @@ -55,6 +55,8 @@ describe("Fido2 page script with native WebAuthn support", () => { setupMockedWebAuthnSupport(); beforeAll(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); }); @@ -166,6 +168,8 @@ describe("Fido2 page script with native WebAuthn support", () => { contentType: "json/application", })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); @@ -184,6 +188,8 @@ describe("Fido2 page script with native WebAuthn support", () => { }, })); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-content-script"); expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); diff --git a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts index e354453ca59..af1838ec942 100644 --- a/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts +++ b/apps/browser/src/autofill/fido2/content/fido2-page-script.webauthn-unsupported.spec.ts @@ -50,6 +50,8 @@ describe("Fido2 page script without native WebAuthn support", () => { const mockCreateCredentialsResult = createCreateCredentialResultMock(); const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); const mockCredentialAssertResult = createAssertCredentialResultMock(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./fido2-page-script"); afterEach(() => { diff --git a/apps/browser/src/autofill/models/autofill-field.ts b/apps/browser/src/autofill/models/autofill-field.ts index 7660b4ce5f0..c0be60f1cd0 100644 --- a/apps/browser/src/autofill/models/autofill-field.ts +++ b/apps/browser/src/autofill/models/autofill-field.ts @@ -1,3 +1,4 @@ +import { FieldRect } from "../background/abstractions/overlay.background"; // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { AutofillFieldQualifierType } from "../enums/autofill-field.enums"; @@ -124,4 +125,9 @@ export default class AutofillField { fieldQualifier?: AutofillFieldQualifierType; accountCreationFieldType?: InlineMenuAccountCreationFieldTypes; + + /** + * used for totp multiline calculations + */ + fieldRect?: FieldRect; } diff --git a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts index 425d53783e1..2e38adacb32 100644 --- a/apps/browser/src/autofill/notification/abstractions/notification-bar.ts +++ b/apps/browser/src/autofill/notification/abstractions/notification-bar.ts @@ -1,7 +1,16 @@ import { Theme } from "@bitwarden/common/platform/enums"; +const NotificationTypes = { + Add: "add", + Change: "change", + Unlock: "unlock", + FilelessImport: "fileless-import", +} as const; + +type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes]; + type NotificationBarIframeInitData = { - type?: string; + type?: string; // @TODO use `NotificationType` isVaultLocked?: boolean; theme?: Theme; removeIndividualVault?: boolean; @@ -24,6 +33,8 @@ type NotificationBarWindowMessageHandlers = { }; export { + NotificationTypes, + NotificationType, NotificationBarIframeInitData, NotificationBarWindowMessage, NotificationBarWindowMessageHandlers, diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts index a4b8ae44b6a..2c0ebe8e8e7 100644 --- a/apps/browser/src/autofill/notification/bar.ts +++ b/apps/browser/src/autofill/notification/bar.ts @@ -15,6 +15,8 @@ import { NotificationBarIframeInitData, } from "./abstractions/notification-bar"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./bar.scss"); const logService = new ConsoleLogService(false); diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts index 0ed14a520c1..36ef3897c56 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/button/bootstrap-autofill-inline-menu-button.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import { AutofillInlineMenuButton } from "./autofill-inline-menu-button"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./button.scss"); (function () { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts index c302c50b4a4..b46b208b084 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/list/bootstrap-autofill-inline-menu-list.ts @@ -2,6 +2,8 @@ import { AutofillOverlayElement } from "../../../../enums/autofill-overlay.enum" import { AutofillInlineMenuList } from "./autofill-inline-menu-list"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./list.scss"); (function () { diff --git a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts index 16d5c29d574..522b968e533 100644 --- a/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts +++ b/apps/browser/src/autofill/overlay/inline-menu/pages/menu-container/bootstrap-autofill-inline-menu-container.ts @@ -1,3 +1,5 @@ +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./menu-container.scss"); import { AutofillInlineMenuContainer } from "./autofill-inline-menu-container"; diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html deleted file mode 100644 index 852fd4a0e81..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.html +++ /dev/null @@ -1,36 +0,0 @@ -
-
- -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts deleted file mode 100644 index 2101979fe36..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-cipher-row-v1.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; - -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; - -@Component({ - selector: "app-fido2-cipher-row-v1", - templateUrl: "fido2-cipher-row-v1.component.html", - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class Fido2CipherRowV1Component { - @Output() onSelected = new EventEmitter(); - @Input() cipher: CipherView; - @Input() last: boolean; - @Input() title: string; - @Input() isSearching: boolean; - @Input() isSelected: boolean; - - protected selectCipher(c: CipherView) { - this.onSelected.emit(c); - } - - /** - * Returns a subname for the cipher. - * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. - * @param c Cipher - * @returns - */ - protected getSubName(c: CipherView): string | null { - const fido2Credentials = c.login?.fido2Credentials; - - if (!fido2Credentials || fido2Credentials.length === 0) { - return null; - } - - const [fido2Credential] = fido2Credentials; - - return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html deleted file mode 100644 index 9f6c0aca50d..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.html +++ /dev/null @@ -1,52 +0,0 @@ - - - - -
- -
-
- -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts deleted file mode 100644 index b7a1bc195ad..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-use-browser-link-v1.component.ts +++ /dev/null @@ -1,115 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { fido2PopoutSessionData$ } from "../../../vault/popup/utils/fido2-popout-session-data"; -import { BrowserFido2UserInterfaceSession } from "../../fido2/services/browser-fido2-user-interface.service"; - -@Component({ - selector: "app-fido2-use-browser-link-v1", - templateUrl: "fido2-use-browser-link-v1.component.html", - animations: [ - trigger("transformPanel", [ - state( - "void", - style({ - opacity: 0, - }), - ), - transition( - "void => open", - animate( - "100ms linear", - style({ - opacity: 1, - }), - ), - ), - transition("* => void", animate("100ms linear", style({ opacity: 0 }))), - ]), - ], -}) -export class Fido2UseBrowserLinkV1Component { - showOverlay = false; - isOpen = false; - overlayPosition: ConnectedPosition[] = [ - { - originX: "start", - originY: "bottom", - overlayX: "start", - overlayY: "top", - offsetY: 5, - }, - ]; - - protected fido2PopoutSessionData$ = fido2PopoutSessionData$(); - - constructor( - private domainSettingsService: DomainSettingsService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - toggle() { - this.isOpen = !this.isOpen; - } - - close() { - this.isOpen = false; - } - - /** - * Aborts the current FIDO2 session and fallsback to the browser. - * @param excludeDomain - Identifies if the domain should be excluded from future FIDO2 prompts. - */ - protected async abort(excludeDomain = true) { - this.close(); - const sessionData = await firstValueFrom(this.fido2PopoutSessionData$); - - if (!excludeDomain) { - this.abortSession(sessionData.sessionId); - return; - } - // Show overlay to prevent the user from interacting with the page. - this.showOverlay = true; - await this.handleDomainExclusion(sessionData.senderUrl); - // Give the user a chance to see the toast before closing the popout. - await Utils.delay(2000); - this.abortSession(sessionData.sessionId); - } - - /** - * Excludes the domain from future FIDO2 prompts. - * @param uri - The domain uri to exclude from future FIDO2 prompts. - */ - private async handleDomainExclusion(uri: string) { - const existingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - - const validDomain = Utils.getHostname(uri); - const savedDomains: NeverDomains = { - ...existingDomains, - }; - savedDomains[validDomain] = null; - - await this.domainSettingsService.setNeverDomains(savedDomains); - - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("domainAddedToExcludedDomains", validDomain), - ); - } - - private abortSession(sessionId: string) { - BrowserFido2UserInterfaceSession.abortPopout(sessionId, true); - } -} diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html deleted file mode 100644 index 8a052fbc5b7..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.html +++ /dev/null @@ -1,142 +0,0 @@ - -
-
-
- - - - - - -
- - -
- -
-
-
- - - -
-

- {{ subtitleText | i18n }} -

- - -
-
- -
-
- -
- -
-
- - -
- -
-
-
-
- -
-

{{ "passkeyAlreadyExists" | i18n }}

-
-
- -
-
- -
-
- -
-

{{ "noPasskeysFoundForThisApplication" | i18n }}

-
- -
-
- - -
-
diff --git a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts b/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts deleted file mode 100644 index 8dc603cfe29..00000000000 --- a/apps/browser/src/autofill/popup/fido2/fido2-v1.component.ts +++ /dev/null @@ -1,445 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { - BehaviorSubject, - combineLatest, - concatMap, - filter, - firstValueFrom, - map, - Observable, - Subject, - take, - takeUntil, -} from "rxjs"; - -import { SearchService } from "@bitwarden/common/abstractions/search.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums"; -import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; -import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; -import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; - -import { ZonedMessageListenerService } from "../../../platform/browser/zoned-message-listener.service"; -import { VaultPopoutType } from "../../../vault/popup/utils/vault-popout-window"; -import { Fido2UserVerificationService } from "../../../vault/services/fido2-user-verification.service"; -import { - BrowserFido2Message, - BrowserFido2UserInterfaceSession, - BrowserFido2MessageTypes, -} from "../../fido2/services/browser-fido2-user-interface.service"; - -interface ViewData { - message: BrowserFido2Message; - fallbackSupported: boolean; -} - -@Component({ - selector: "app-fido2-v1", - templateUrl: "fido2-v1.component.html", - styleUrls: [], -}) -export class Fido2V1Component implements OnInit, OnDestroy { - private destroy$ = new Subject(); - private hasSearched = false; - - protected cipher: CipherView; - protected searchTypeSearch = false; - protected searchPending = false; - protected searchText: string; - protected url: string; - protected hostname: string; - protected data$: Observable; - protected sessionId?: string; - protected senderTabId?: string; - protected ciphers?: CipherView[] = []; - protected displayedCiphers?: CipherView[] = []; - protected loading = false; - protected subtitleText: string; - protected credentialText: string; - protected BrowserFido2MessageTypes = BrowserFido2MessageTypes; - - private message$ = new BehaviorSubject(null); - - constructor( - private router: Router, - private activatedRoute: ActivatedRoute, - private cipherService: CipherService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private searchService: SearchService, - private logService: LogService, - private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, - private passwordRepromptService: PasswordRepromptService, - private fido2UserVerificationService: Fido2UserVerificationService, - private accountService: AccountService, - ) {} - - ngOnInit() { - this.searchTypeSearch = !this.platformUtilsService.isSafari(); - - const queryParams$ = this.activatedRoute.queryParamMap.pipe( - take(1), - map((queryParamMap) => ({ - sessionId: queryParamMap.get("sessionId"), - senderTabId: queryParamMap.get("senderTabId"), - senderUrl: queryParamMap.get("senderUrl"), - })), - ); - - combineLatest([ - queryParams$, - this.browserMessagingApi.messageListener$() as Observable, - ]) - .pipe( - concatMap(async ([queryParams, message]) => { - this.sessionId = queryParams.sessionId; - this.senderTabId = queryParams.senderTabId; - this.url = queryParams.senderUrl; - // For a 'NewSessionCreatedRequest', abort if it doesn't belong to the current session. - if ( - message.type === BrowserFido2MessageTypes.NewSessionCreatedRequest && - message.sessionId !== queryParams.sessionId - ) { - this.abort(false); - return; - } - - // Ignore messages that don't belong to the current session. - if (message.sessionId !== queryParams.sessionId) { - return; - } - - if (message.type === BrowserFido2MessageTypes.AbortRequest) { - this.abort(false); - return; - } - - return message; - }), - filter((message) => !!message), - takeUntil(this.destroy$), - ) - .subscribe((message) => { - this.message$.next(message); - }); - - this.data$ = this.message$.pipe( - filter((message) => message != undefined), - concatMap(async (message) => { - switch (message.type) { - case BrowserFido2MessageTypes.ConfirmNewCredentialRequest: { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - - this.ciphers = (await this.cipherService.getAllDecrypted()).filter( - (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, - ); - this.displayedCiphers = this.ciphers.filter( - (cipher) => - cipher.login.matchesUri(this.url, equivalentDomains) && - this.hasNoOtherPasskeys(cipher, message.userHandle), - ); - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.PickCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.cipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - - case BrowserFido2MessageTypes.InformExcludedCredentialRequest: { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.ciphers = await Promise.all( - message.existingCipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); - return cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), - ); - }), - ); - this.displayedCiphers = [...this.ciphers]; - - if (this.displayedCiphers.length > 0) { - this.selectedPasskey(this.displayedCiphers[0]); - } - break; - } - } - - this.subtitleText = - this.displayedCiphers.length > 0 - ? this.getCredentialSubTitleText(message.type) - : "noMatchingPasskeyLogin"; - - this.credentialText = this.getCredentialButtonText(message.type); - return { - message, - fallbackSupported: "fallbackSupported" in message && message.fallbackSupported, - }; - }), - takeUntil(this.destroy$), - ); - - queryParams$.pipe(takeUntil(this.destroy$)).subscribe((queryParams) => { - this.send({ - sessionId: queryParams.sessionId, - type: BrowserFido2MessageTypes.ConnectResponse, - }); - }); - } - - protected async submit() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.PickCredentialRequest) { - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.PickCredentialResponse, - userVerified, - }); - } else if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - if (this.cipher.login.hasFido2Credentials) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "overwritePasskey" }, - content: { key: "overwritePasskeyAlert" }, - type: "info", - }); - - if (!confirmed) { - return false; - } - } - - // TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - const userVerified = await this.handleUserVerification(data.userVerification, this.cipher); - - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified, - }); - } - - this.loading = true; - } - - protected async saveNewLogin() { - const data = this.message$.value; - if (data?.type === BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - const name = data.credentialName || data.rpId; - // TODO: Revert to check for user verification once user verification for passkeys is approved for production. - // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - await this.createNewCipher(name, data.userName); - - // We are bypassing user verification pending approval. - this.send({ - sessionId: this.sessionId, - cipherId: this.cipher?.id, - type: BrowserFido2MessageTypes.ConfirmNewCredentialResponse, - userVerified: data.userVerification, - }); - } - - this.loading = true; - } - - getCredentialSubTitleText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "chooseCipherForPasskeySave" - : "logInWithPasskeyQuestion"; - } - - getCredentialButtonText(messageType: string): string { - return messageType == BrowserFido2MessageTypes.ConfirmNewCredentialRequest - ? "savePasskey" - : "confirm"; - } - - selectedPasskey(item: CipherView) { - this.cipher = item; - } - - viewPasskey() { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { - queryParams: { - cipherId: this.cipher.id, - uilocation: "popout", - senderTabId: this.senderTabId, - sessionId: this.sessionId, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - addCipher() { - const data = this.message$.value; - - if (data?.type !== BrowserFido2MessageTypes.ConfirmNewCredentialRequest) { - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/add-cipher"], { - queryParams: { - name: data.credentialName || data.rpId, - uri: this.url, - type: CipherType.Login.toString(), - uilocation: "popout", - username: data.userName, - senderTabId: this.senderTabId, - sessionId: this.sessionId, - userVerification: data.userVerification, - singleActionPopout: `${VaultPopoutType.fido2Popout}_${this.sessionId}`, - }, - }); - } - - protected async search() { - this.hasSearched = await this.searchService.isSearchable(this.searchText); - this.searchPending = true; - if (this.hasSearched) { - this.displayedCiphers = await this.searchService.searchCiphers( - this.searchText, - null, - this.ciphers, - ); - } else { - const equivalentDomains = await firstValueFrom( - this.domainSettingsService.getUrlEquivalentDomains(this.url), - ); - this.displayedCiphers = this.ciphers.filter((cipher) => - cipher.login.matchesUri(this.url, equivalentDomains), - ); - } - this.searchPending = false; - this.selectedPasskey(this.displayedCiphers[0]); - } - - abort(fallback: boolean) { - this.unload(fallback); - window.close(); - } - - unload(fallback = false) { - this.send({ - sessionId: this.sessionId, - type: BrowserFido2MessageTypes.AbortResponse, - fallbackRequested: fallback, - }); - } - - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); - } - - private buildCipher(name: string, username: string) { - this.cipher = new CipherView(); - this.cipher.name = name; - - this.cipher.type = CipherType.Login; - this.cipher.login = new LoginView(); - this.cipher.login.username = username; - this.cipher.login.uris = [new LoginUriView()]; - this.cipher.login.uris[0].uri = this.url; - this.cipher.card = new CardView(); - this.cipher.identity = new IdentityView(); - this.cipher.secureNote = new SecureNoteView(); - this.cipher.secureNote.type = SecureNoteType.Generic; - this.cipher.reprompt = CipherRepromptType.None; - } - - private async createNewCipher(name: string, username: string) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - - this.buildCipher(name, username); - const cipher = await this.cipherService.encrypt(this.cipher, activeUserId); - try { - await this.cipherService.createWithServer(cipher); - this.cipher.id = cipher.id; - } catch (e) { - this.logService.error(e); - } - } - - // TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production. - private async handleUserVerification( - userVerificationRequested: boolean, - cipher: CipherView, - ): Promise { - const masterPasswordRepromptRequired = cipher && cipher.reprompt !== 0; - - if (masterPasswordRepromptRequired) { - return await this.passwordRepromptService.showPasswordPrompt(); - } - - return userVerificationRequested; - } - - private send(msg: BrowserFido2Message) { - BrowserFido2UserInterfaceSession.sendMessage({ - sessionId: this.sessionId, - ...msg, - }); - } - - /** - * This methods returns true if a cipher either has no passkeys, or has a passkey matching with userHandle - * @param userHandle - */ - private hasNoOtherPasskeys(cipher: CipherView, userHandle: string): boolean { - if (cipher.login.fido2Credentials == null || cipher.login.fido2Credentials.length === 0) { - return true; - } - - return cipher.login.fido2Credentials.some((passkey) => passkey.userHandle === userHandle); - } -} diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html b/apps/browser/src/autofill/popup/settings/autofill-v1.component.html deleted file mode 100644 index 1c16ee1fe12..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.html +++ /dev/null @@ -1,270 +0,0 @@ -
-
- -
-

- {{ "autofill" | i18n }} -

-
-
-
-
-
- -
- -
-
-
-
- - -
- -
-
-
-
-
- - -
-
- - -
-
-
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-

{{ "additionalOptions" | i18n }}

-
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- -
-
-
- -
-
-
diff --git a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts b/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts deleted file mode 100644 index 9f015d990e9..00000000000 --- a/apps/browser/src/autofill/popup/settings/autofill-v1.component.ts +++ /dev/null @@ -1,344 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; -import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { - InlineMenuVisibilitySetting, - ClearClipboardDelaySetting, -} from "@bitwarden/common/autofill/types"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { - UriMatchStrategy, - UriMatchStrategySetting, -} from "@bitwarden/common/models/domain/domain-service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { DialogService } from "@bitwarden/components"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "app-autofill-v1", - templateUrl: "autofill-v1.component.html", -}) -export class AutofillV1Component implements OnInit { - protected canOverrideBrowserAutofillSetting = false; - protected defaultBrowserAutofillDisabled = false; - protected autoFillOverlayVisibility: InlineMenuVisibilitySetting; - protected autoFillOverlayVisibilityOptions: any[]; - protected disablePasswordManagerLink: string; - protected inlineMenuPositioningImprovementsEnabled: boolean = false; - protected blockBrowserInjectionsByDomainEnabled: boolean = false; - protected showInlineMenuIdentities: boolean = true; - protected showInlineMenuCards: boolean = true; - inlineMenuIsEnabled: boolean = false; - enableAutoFillOnPageLoad = false; - autoFillOnPageLoadDefault = false; - autoFillOnPageLoadOptions: any[]; - enableContextMenuItem = false; - enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? - clearClipboard: ClearClipboardDelaySetting; - clearClipboardOptions: any[]; - defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; - uriMatchOptions: any[]; - showCardsCurrentTab = false; - showIdentitiesCurrentTab = false; - autofillKeyboardHelperText: string; - accountSwitcherEnabled = false; - - constructor( - private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, - private domainSettingsService: DomainSettingsService, - private configService: ConfigService, - private dialogService: DialogService, - private autofillSettingsService: AutofillSettingsServiceAbstraction, - private messagingService: MessagingService, - private vaultSettingsService: VaultSettingsService, - ) { - this.autoFillOverlayVisibilityOptions = [ - { - name: i18nService.t("autofillOverlayVisibilityOff"), - value: AutofillOverlayVisibility.Off, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnFieldFocus"), - value: AutofillOverlayVisibility.OnFieldFocus, - }, - { - name: i18nService.t("autofillOverlayVisibilityOnButtonClick"), - value: AutofillOverlayVisibility.OnButtonClick, - }, - ]; - this.autoFillOnPageLoadOptions = [ - { name: i18nService.t("autoFillOnPageLoadYes"), value: true }, - { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, - ]; - this.clearClipboardOptions = [ - { name: i18nService.t("never"), value: null }, - { name: i18nService.t("tenSeconds"), value: 10 }, - { name: i18nService.t("twentySeconds"), value: 20 }, - { name: i18nService.t("thirtySeconds"), value: 30 }, - { name: i18nService.t("oneMinute"), value: 60 }, - { name: i18nService.t("twoMinutes"), value: 120 }, - { name: i18nService.t("fiveMinutes"), value: 300 }, - ]; - this.uriMatchOptions = [ - { name: i18nService.t("baseDomainOptionRecommended"), value: UriMatchStrategy.Domain }, - { name: i18nService.t("host"), value: UriMatchStrategy.Host }, - { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, - { name: i18nService.t("never"), value: UriMatchStrategy.Never }, - ]; - - this.accountSwitcherEnabled = enableAccountSwitching(); - this.disablePasswordManagerLink = this.getDisablePasswordManagerLink(); - } - - async ngOnInit() { - this.canOverrideBrowserAutofillSetting = - this.platformUtilsService.isChrome() || - this.platformUtilsService.isEdge() || - this.platformUtilsService.isOpera() || - this.platformUtilsService.isVivaldi(); - - this.defaultBrowserAutofillDisabled = await this.browserAutofillSettingCurrentlyOverridden(); - - this.autoFillOverlayVisibility = await firstValueFrom( - this.autofillSettingsService.inlineMenuVisibility$, - ); - - this.inlineMenuPositioningImprovementsEnabled = await this.configService.getFeatureFlag( - FeatureFlag.InlineMenuPositioningImprovements, - ); - - this.blockBrowserInjectionsByDomainEnabled = await this.configService.getFeatureFlag( - FeatureFlag.BlockBrowserInjectionsByDomain, - ); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - - this.showInlineMenuIdentities = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuIdentities$)); - - this.showInlineMenuCards = - this.inlineMenuPositioningImprovementsEnabled && - (await firstValueFrom(this.autofillSettingsService.showInlineMenuCards$)); - - this.enableAutoFillOnPageLoad = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoad$, - ); - - this.autoFillOnPageLoadDefault = await firstValueFrom( - this.autofillSettingsService.autofillOnPageLoadDefault$, - ); - - this.enableContextMenuItem = await firstValueFrom( - this.autofillSettingsService.enableContextMenu$, - ); - - this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); - - this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); - - const defaultUriMatch = await firstValueFrom( - this.domainSettingsService.defaultUriMatchStrategy$, - ); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; - - const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); - await this.setAutofillKeyboardHelperText(command); - - this.showCardsCurrentTab = await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$); - - this.showIdentitiesCurrentTab = await firstValueFrom( - this.vaultSettingsService.showIdentitiesCurrentTab$, - ); - } - - isInlineMenuEnabled() { - return ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnFieldFocus || - this.autoFillOverlayVisibility === AutofillOverlayVisibility.OnButtonClick - ); - } - - async updateAutoFillOverlayVisibility() { - await this.autofillSettingsService.setInlineMenuVisibility(this.autoFillOverlayVisibility); - await this.requestPrivacyPermission(); - - this.inlineMenuIsEnabled = this.isInlineMenuEnabled(); - } - - async updateAutoFillOnPageLoad() { - await this.autofillSettingsService.setAutofillOnPageLoad(this.enableAutoFillOnPageLoad); - } - - async updateAutoFillOnPageLoadDefault() { - await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); - } - - async saveDefaultUriMatch() { - await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); - } - - private async setAutofillKeyboardHelperText(command: string) { - if (command) { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutText", command); - } else { - this.autofillKeyboardHelperText = this.i18nService.t("autofillLoginShortcutNotSet"); - } - } - - async commandSettings() { - if (this.platformUtilsService.isChrome()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("chrome://extensions/shortcuts"); - } else if (this.platformUtilsService.isOpera()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("opera://extensions/shortcuts"); - } else if (this.platformUtilsService.isEdge()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("edge://extensions/shortcuts"); - } else if (this.platformUtilsService.isVivaldi()) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("vivaldi://extensions/shortcuts"); - } else { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab("https://bitwarden.com/help/keyboard-shortcuts"); - } - } - - private getDisablePasswordManagerLink(): string { - if (this.platformUtilsService.isChrome()) { - return "chrome://settings/autofill"; - } - if (this.platformUtilsService.isOpera()) { - return "opera://settings/autofill"; - } - if (this.platformUtilsService.isEdge()) { - return "edge://settings/passwords"; - } - if (this.platformUtilsService.isVivaldi()) { - return "vivaldi://settings/autofill"; - } - - return "https://bitwarden.com/help/disable-browser-autofill/"; - } - - protected openDisablePasswordManagerLink(event: Event) { - event.preventDefault(); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - BrowserApi.createNewTab(this.disablePasswordManagerLink); - } - - async requestPrivacyPermission() { - if ( - this.autoFillOverlayVisibility === AutofillOverlayVisibility.Off || - !this.canOverrideBrowserAutofillSetting || - (await this.browserAutofillSettingCurrentlyOverridden()) - ) { - return; - } - - await this.dialogService.openSimpleDialog({ - title: { key: "overrideDefaultBrowserAutofillTitle" }, - content: { key: "overrideDefaultBrowserAutofillDescription" }, - acceptButtonText: { key: "makeDefault" }, - acceptAction: async () => await this.handleOverrideDialogAccept(), - cancelButtonText: { key: "ignore" }, - type: "info", - }); - } - - async updateDefaultBrowserAutofillDisabled() { - const privacyPermissionGranted = await this.privacyPermissionGranted(); - if (!this.defaultBrowserAutofillDisabled && !privacyPermissionGranted) { - return; - } - - if ( - !privacyPermissionGranted && - !(await BrowserApi.requestPermission({ permissions: ["privacy"] })) - ) { - await this.dialogService.openSimpleDialog({ - title: { key: "privacyPermissionAdditionNotGrantedTitle" }, - content: { key: "privacyPermissionAdditionNotGrantedDescription" }, - acceptButtonText: { key: "ok" }, - cancelButtonText: null, - type: "warning", - }); - this.defaultBrowserAutofillDisabled = false; - - return; - } - - BrowserApi.updateDefaultBrowserAutofillSettings(!this.defaultBrowserAutofillDisabled); - } - - private handleOverrideDialogAccept = async () => { - this.defaultBrowserAutofillDisabled = true; - await this.updateDefaultBrowserAutofillDisabled(); - }; - - async browserAutofillSettingCurrentlyOverridden() { - if (!this.canOverrideBrowserAutofillSetting) { - return false; - } - - if (!(await this.privacyPermissionGranted())) { - return false; - } - - return await BrowserApi.browserAutofillSettingsOverridden(); - } - - async privacyPermissionGranted(): Promise { - return await BrowserApi.permissionsGranted(["privacy"]); - } - - async updateContextMenuItem() { - await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); - this.messagingService.send("bgUpdateContextMenu"); - } - - async updateAutoTotpCopy() { - await this.autofillSettingsService.setAutoCopyTotp(this.enableAutoTotpCopy); - } - - async saveClearClipboard() { - await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); - } - - async updateShowCardsCurrentTab() { - await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); - } - - async updateShowIdentitiesCurrentTab() { - await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); - } - - async updateShowInlineMenuCards() { - await this.autofillSettingsService.setShowInlineMenuCards(this.showInlineMenuCards); - } - - async updateShowInlineMenuIdentities() { - await this.autofillSettingsService.setShowInlineMenuIdentities(this.showInlineMenuIdentities); - } -} diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html deleted file mode 100644 index 8f78ac16404..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.html +++ /dev/null @@ -1,91 +0,0 @@ -
-
-
- -
-

- {{ "excludedDomains" | i18n }} -

-
- -
-
-
-
-
- - -
- -
- - - - -
-
- -
-
-
- -
-
-
-
diff --git a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts b/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts deleted file mode 100644 index 1a7dcd9c2dc..00000000000 --- a/apps/browser/src/autofill/popup/settings/excluded-domains-v1.component.ts +++ /dev/null @@ -1,143 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; -import { Router } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../../platform/browser/browser-api"; -import { enableAccountSwitching } from "../../../platform/flags"; - -interface ExcludedDomain { - uri: string; - showCurrentUris: boolean; -} - -const BroadcasterSubscriptionId = "excludedDomains"; - -@Component({ - selector: "app-excluded-domains-v1", - templateUrl: "excluded-domains-v1.component.html", -}) -export class ExcludedDomainsV1Component implements OnInit, OnDestroy { - excludedDomains: ExcludedDomain[] = []; - existingExcludedDomains: ExcludedDomain[] = []; - currentUris: string[]; - loadCurrentUrisTimeout: number; - accountSwitcherEnabled = false; - - constructor( - private domainSettingsService: DomainSettingsService, - private i18nService: I18nService, - private router: Router, - private broadcasterService: BroadcasterService, - private ngZone: NgZone, - private platformUtilsService: PlatformUtilsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); - if (savedDomains) { - for (const uri of Object.keys(savedDomains)) { - this.excludedDomains.push({ uri: uri, showCurrentUris: false }); - this.existingExcludedDomains.push({ uri: uri, showCurrentUris: false }); - } - } - - await this.loadCurrentUris(); - - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.ngZone.run(async () => { - switch (message.command) { - case "tabChanged": - case "windowChanged": - if (this.loadCurrentUrisTimeout != null) { - window.clearTimeout(this.loadCurrentUrisTimeout); - } - this.loadCurrentUrisTimeout = window.setTimeout( - async () => await this.loadCurrentUris(), - 500, - ); - break; - default: - break; - } - }); - }); - } - - ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - } - - async addUri() { - this.excludedDomains.push({ uri: "", showCurrentUris: false }); - } - - async removeUri(i: number) { - this.excludedDomains.splice(i, 1); - } - - async submit() { - const savedDomains: { [name: string]: null } = {}; - const newExcludedDomains = this.getNewlyAddedDomains(this.excludedDomains); - for (const domain of this.excludedDomains) { - const resp = newExcludedDomains.filter((e) => e.uri === domain.uri); - if (resp.length === 0) { - savedDomains[domain.uri] = null; - } else { - if (domain.uri && domain.uri !== "") { - const validDomain = Utils.getHostname(domain.uri); - if (!validDomain) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("excludedDomainsInvalidDomain", domain.uri), - ); - return; - } - savedDomains[validDomain] = null; - } - } - } - - await this.domainSettingsService.setNeverDomains(savedDomains); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/tabs/settings"]); - } - - trackByFunction(index: number, item: any) { - return index; - } - - getNewlyAddedDomains(domain: ExcludedDomain[]): ExcludedDomain[] { - const result = this.excludedDomains.filter( - (newDomain) => - !this.existingExcludedDomains.some((oldDomain) => newDomain.uri === oldDomain.uri), - ); - return result; - } - - toggleUriInput(domain: ExcludedDomain) { - domain.showCurrentUris = !domain.showCurrentUris; - } - - async loadCurrentUris() { - const tabs = await BrowserApi.tabsQuery({ windowType: "normal" }); - if (tabs) { - const uriSet = new Set(tabs.map((tab) => Utils.getHostname(tab.url))); - uriSet.delete(null); - this.currentUris = Array.from(uriSet); - } - } -} diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html b/apps/browser/src/autofill/popup/settings/notifications-v1.component.html deleted file mode 100644 index 89d83c9e480..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.html +++ /dev/null @@ -1,89 +0,0 @@ -
-
- -
-

- {{ "notifications" | i18n }} -

-
- -
-
-
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
-
- - -
-
- -
-
-
- -
-
-
diff --git a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts b/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts deleted file mode 100644 index 442e91e55eb..00000000000 --- a/apps/browser/src/autofill/popup/settings/notifications-v1.component.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Component, OnInit } from "@angular/core"; -import { firstValueFrom } from "rxjs"; - -import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; -import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; - -import { enableAccountSwitching } from "../../../platform/flags"; - -@Component({ - selector: "autofill-notification-v1-settings", - templateUrl: "notifications-v1.component.html", -}) -export class NotificationsSettingsV1Component implements OnInit { - enableAddLoginNotification = false; - enableChangedPasswordNotification = false; - enablePasskeys = true; - accountSwitcherEnabled = false; - - constructor( - private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, - private vaultSettingsService: VaultSettingsService, - ) { - this.accountSwitcherEnabled = enableAccountSwitching(); - } - - async ngOnInit() { - this.enableAddLoginNotification = await firstValueFrom( - this.userNotificationSettingsService.enableAddedLoginPrompt$, - ); - - this.enableChangedPasswordNotification = await firstValueFrom( - this.userNotificationSettingsService.enableChangedPasswordPrompt$, - ); - - this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); - } - - async updateAddLoginNotification() { - await this.userNotificationSettingsService.setEnableAddedLoginPrompt( - this.enableAddLoginNotification, - ); - } - - async updateChangedPasswordNotification() { - await this.userNotificationSettingsService.setEnableChangedPasswordPrompt( - this.enableChangedPasswordNotification, - ); - } - - async updateEnablePasskeys() { - await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys); - } -} diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index daa65d74ae6..acf0dedde27 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -957,8 +957,17 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ accountCreationFieldType: autofillFieldData?.accountCreationFieldType, }; + const allFields = this.formFieldElements; + const allFieldsRect = []; + + for (const key of allFields.keys()) { + const rect = await this.getMostRecentlyFocusedFieldRects(key); + allFieldsRect.push({ ...allFields.get(key), rect }); // Add the combined result to the array + } + await this.sendExtensionMessage("updateFocusedFieldData", { focusedFieldData: this.focusedFieldData, + allFieldsRect, }); } @@ -1408,6 +1417,8 @@ export class AutofillOverlayContentService implements AutofillOverlayContentServ url.origin + pathWithoutTrailingSlashAndSearch, url.origin + pathWithoutTrailingSlashSearchAndHash, ]); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (_error) { return null; } diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 77d73d7ae65..16b11b98866 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -1,4 +1,4 @@ -import { mock, mockReset, MockProxy } from "jest-mock-extended"; +import { mock, MockProxy, mockReset } from "jest-mock-extended"; import { BehaviorSubject, of, Subject } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; @@ -730,7 +730,9 @@ describe("AutofillService", () => { it("throws an error if an autofill did not occur for any of the passed pages", async () => { autofillOptions.tab.url = "https://a-different-url.com"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); try { await autofillService.doAutoFill(autofillOptions); @@ -912,7 +914,9 @@ describe("AutofillService", () => { it("returns a TOTP value", async () => { const totpCode = "123456"; autofillOptions.cipher.login.totp = "totp"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); jest.spyOn(totpService, "getCode").mockResolvedValue(totpCode); @@ -925,7 +929,9 @@ describe("AutofillService", () => { it("does not return a TOTP value if the user does not have premium features", async () => { autofillOptions.cipher.login.totp = "totp"; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(false)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(true); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -959,7 +965,9 @@ describe("AutofillService", () => { it("returns a null value if the user cannot access premium and the organization does not use TOTP", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = false; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(false)); const autofillResult = await autofillService.doAutoFill(autofillOptions); @@ -969,7 +977,9 @@ describe("AutofillService", () => { it("returns a null value if the user has disabled `auto TOTP copy`", async () => { autofillOptions.cipher.login.totp = "totp"; autofillOptions.cipher.organizationUseTotp = true; - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + jest + .spyOn(billingAccountProfileStateService, "hasPremiumFromAnySource$") + .mockImplementation(() => of(true)); jest.spyOn(autofillService, "getShouldAutoCopyTotp").mockResolvedValue(false); jest.spyOn(totpService, "getCode"); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 093f4bfb638..6d0e9954ade 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -416,8 +416,9 @@ export default class AutofillService implements AutofillServiceInterface { let totp: string | null = null; + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), ); const defaultUriMatch = await this.getDefaultUriMatchStrategy(); diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index 0dbc246b235..16310397a03 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -235,6 +235,8 @@ export class DomQueryService implements DomQueryServiceInterface { if ((chrome as any).dom?.openOrClosedShadowRoot) { try { return (chrome as any).dom.openOrClosedShadowRoot(node); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { return null; } diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index a14ef1330cc..58ac95e7edf 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,5 @@ +import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; -import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeout } from "@bitwarden/common/types/vault-timeout.type"; import { CipherType } from "@bitwarden/common/vault/enums"; diff --git a/apps/browser/src/autofill/utils/index.ts b/apps/browser/src/autofill/utils/index.ts index 5922e26e11b..0e102dcfd99 100644 --- a/apps/browser/src/autofill/utils/index.ts +++ b/apps/browser/src/autofill/utils/index.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { FieldRect } from "../background/abstractions/overlay.background"; import { AutofillPort } from "../enums/autofill-port.enum"; import { FillableFormFieldElement, FormElementWithAttribute, FormFieldElement } from "../types"; @@ -544,3 +545,31 @@ export const specialCharacterToKeyMap: Record = { "?": "questionCharacterDescriptor", "/": "forwardSlashCharacterDescriptor", }; + +/** + * Determines if the current rect values are not all 0. + */ +export function rectHasSize(rect: FieldRect): boolean { + if (rect.right > 0 && rect.left > 0 && rect.top > 0 && rect.bottom > 0) { + return true; + } + + return false; +} + +/** + * Checks if all the values corresponding to the specified keys in an object are null. + * If no keys are specified, checks all keys in the object. + * + * @param obj - The object to check. + * @param keys - An optional array of keys to check in the object. Defaults to all keys. + * @returns Returns true if all values for the specified keys (or all keys if none are provided) are null; otherwise, false. + */ +export function areKeyValuesNull>( + obj: T, + keys?: Array, +): boolean { + const keysToCheck = keys && keys.length > 0 ? keys : (Object.keys(obj) as Array); + + return keysToCheck.every((key) => obj[key] == null); +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 34c10508485..98f3867e5ff 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -204,6 +204,7 @@ import { BiometricStateService, BiometricsService, DefaultBiometricStateService, + DefaultKeyService, DefaultKdfConfigService, KdfConfigService, KeyService as KeyServiceAbstraction, @@ -241,7 +242,6 @@ import AutofillService from "../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../autofill/services/inline-menu-field-qualification.service"; import { SafariApp } from "../browser/safariApp"; import { BackgroundBrowserBiometricsService } from "../key-management/biometrics/background-browser-biometrics.service"; -import { BrowserKeyService } from "../key-management/browser-key.service"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; @@ -416,6 +416,7 @@ export default class MainBackground { await this.refreshMenu(true); if (this.systemService != null) { await this.systemService.clearPendingClipboard(); + await this.biometricsService.setShouldAutopromptNow(false); await this.processReloadService.startProcessReload(this.authService); } }; @@ -631,10 +632,6 @@ export default class MainBackground { this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); - this.biometricsService = new BackgroundBrowserBiometricsService( - runtimeNativeMessagingBackground, - ); - this.kdfConfigService = new DefaultKdfConfigService(this.stateProvider); this.pinService = new PinService( @@ -649,7 +646,7 @@ export default class MainBackground { this.stateService, ); - this.keyService = new BrowserKeyService( + this.keyService = new DefaultKeyService( this.pinService, this.masterPasswordService, this.keyGenerationService, @@ -660,11 +657,16 @@ export default class MainBackground { this.stateService, this.accountService, this.stateProvider, - this.biometricStateService, - this.biometricsService, this.kdfConfigService, ); + this.biometricsService = new BackgroundBrowserBiometricsService( + runtimeNativeMessagingBackground, + this.logService, + this.keyService, + this.biometricStateService, + ); + this.appIdService = new AppIdService(this.storageService, this.logService); this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); @@ -769,7 +771,10 @@ export default class MainBackground { this.configService, ); - this.devicesService = new DevicesServiceImplementation(this.devicesApiService); + this.devicesService = new DevicesServiceImplementation( + this.devicesApiService, + this.appIdService, + ); this.authRequestService = new AuthRequestService( this.appIdService, @@ -792,6 +797,8 @@ export default class MainBackground { this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService( this.stateProvider, + this.platformUtilsService, + this.apiService, ); this.ssoLoginService = new SsoLoginService(this.stateProvider); @@ -852,10 +859,8 @@ export default class MainBackground { this.userVerificationApiService, this.userDecryptionOptionsService, this.pinService, - this.logService, - this.vaultTimeoutSettingsService, - this.platformUtilsService, this.kdfConfigService, + this.biometricsService, ); this.vaultFilterService = new VaultFilterService( @@ -885,6 +890,7 @@ export default class MainBackground { this.stateEventRunnerService, this.taskSchedulerService, this.logService, + this.biometricsService, lockedCallback, logoutCallback, ); @@ -1076,6 +1082,7 @@ export default class MainBackground { this.vaultTimeoutSettingsService, this.biometricStateService, this.accountService, + this.logService, ); // Other fields @@ -1229,6 +1236,7 @@ export default class MainBackground { this.i18nService, this.logService, this.billingAccountProfileStateService, + this.accountService, ); this.cipherContextMenuHandler = new CipherContextMenuHandler( diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index 2ded1760235..b08f1c8b566 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -1,10 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -14,18 +13,19 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey } from "@bitwarden/common/types/key"; -import { KeyService, BiometricStateService } from "@bitwarden/key-management"; +import { KeyService, BiometricStateService, BiometricsCommands } from "@bitwarden/key-management"; import { BrowserApi } from "../platform/browser/browser-api"; import RuntimeBackground from "./runtime.background"; const MessageValidTimeout = 10 * 1000; +const MessageNoResponseTimeout = 60 * 1000; const HashAlgorithmForEncryption = "sha1"; type Message = { command: string; + messageId?: number; // Filled in by this service userId?: string; @@ -43,6 +43,7 @@ type OuterMessage = { type ReceiveMessage = { timestamp: number; command: string; + messageId: number; response?: any; // Unlock key @@ -53,19 +54,23 @@ type ReceiveMessage = { type ReceiveMessageOuter = { command: string; appId: string; + messageId?: number; // Should only have one of these. message?: EncString; sharedSecret?: string; }; +type Callback = { + resolver: any; + rejecter: any; +}; + export class NativeMessagingBackground { - private connected = false; + connected = false; private connecting: boolean; private port: browser.runtime.Port | chrome.runtime.Port; - private resolver: any = null; - private rejecter: any = null; private privateKey: Uint8Array = null; private publicKey: Uint8Array = null; private secureSetupResolve: any = null; @@ -73,6 +78,11 @@ export class NativeMessagingBackground { private appId: string; private validatingFingerprint: boolean; + private messageId = 0; + private callbacks = new Map(); + + isConnectedToOutdatedDesktopClient = true; + constructor( private keyService: KeyService, private encryptService: EncryptService, @@ -97,6 +107,7 @@ export class NativeMessagingBackground { } async connect() { + this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app..."); this.appId = await this.appIdService.getAppId(); await this.biometricStateService.setFingerprintValidated(false); @@ -106,6 +117,9 @@ export class NativeMessagingBackground { this.connecting = true; const connectedCallback = () => { + this.logService.info( + "[Native Messaging IPC] Connection to Bitwarden Desktop app established!", + ); this.connected = true; this.connecting = false; resolve(); @@ -123,11 +137,17 @@ export class NativeMessagingBackground { connectedCallback(); break; case "disconnected": + this.logService.info("[Native Messaging IPC] Disconnected from Bitwarden Desktop app."); if (this.connecting) { reject(new Error("startDesktop")); } this.connected = false; this.port.disconnect(); + // reject all + for (const callback of this.callbacks.values()) { + callback.rejecter("disconnected"); + } + this.callbacks.clear(); break; case "setupEncryption": { // Ignore since it belongs to another device @@ -147,6 +167,16 @@ export class NativeMessagingBackground { await this.biometricStateService.setFingerprintValidated(true); } this.sharedSecret = new SymmetricCryptoKey(decrypted); + this.logService.info("[Native Messaging IPC] Secure channel established"); + + if ("messageId" in message) { + this.logService.info("[Native Messaging IPC] Non-legacy desktop client"); + this.isConnectedToOutdatedDesktopClient = false; + } else { + this.logService.info("[Native Messaging IPC] Legacy desktop client"); + this.isConnectedToOutdatedDesktopClient = true; + } + this.secureSetupResolve(); break; } @@ -155,17 +185,25 @@ export class NativeMessagingBackground { if (message.appId !== this.appId) { return; } + this.logService.warning( + "[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...", + ); this.sharedSecret = null; this.privateKey = null; this.connected = false; - this.rejecter({ - message: "invalidateEncryption", - }); + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId).rejecter({ + message: "invalidateEncryption", + }); + } return; case "verifyFingerprint": { if (this.sharedSecret == null) { + this.logService.info( + "[Native Messaging IPC] Desktop app requested trust verification by fingerprint.", + ); this.validatingFingerprint = true; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -174,9 +212,11 @@ export class NativeMessagingBackground { break; } case "wrongUserId": - this.rejecter({ - message: "wrongUserId", - }); + if (this.callbacks.has(message.messageId)) { + this.callbacks.get(message.messageId).rejecter({ + message: "wrongUserId", + }); + } return; default: // Ignore since it belongs to another device @@ -210,6 +250,60 @@ export class NativeMessagingBackground { }); } + async callCommand(message: Message): Promise { + const messageId = this.messageId++; + + if ( + message.command == BiometricsCommands.Unlock || + message.command == BiometricsCommands.IsAvailable + ) { + // TODO remove after 2025.3 + // wait until there is no other callbacks, or timeout + const call = await firstValueFrom( + race( + from([false]).pipe(delay(5000)), + timer(0, 100).pipe( + filter(() => this.callbacks.size === 0), + map(() => true), + ), + ), + ); + if (!call) { + this.logService.info( + `[Native Messaging IPC] Message of type ${message.command} did not get a response before timing out`, + ); + return; + } + } + + const callback = new Promise((resolver, rejecter) => { + this.callbacks.set(messageId, { resolver, rejecter }); + }); + message.messageId = messageId; + try { + await this.send(message); + } catch (e) { + this.logService.info( + `[Native Messaging IPC] Error sending message of type ${message.command} to Bitwarden Desktop app. Error: ${e}`, + ); + const callback = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + callback.rejecter("errorConnecting"); + } + + setTimeout(() => { + if (this.callbacks.has(messageId)) { + this.logService.info("[Native Messaging IPC] Message timed out and received no response"); + this.callbacks.get(messageId).rejecter({ + message: "timeout", + }); + this.callbacks.delete(messageId); + } + }, MessageNoResponseTimeout); + + return callback; + } + async send(message: Message) { if (!this.connected) { await this.connect(); @@ -233,20 +327,7 @@ export class NativeMessagingBackground { return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret); } - getResponse(): Promise { - return new Promise((resolve, reject) => { - this.resolver = function (response: any) { - resolve(response); - }; - this.rejecter = function (resp: any) { - reject({ - message: resp, - }); - }; - }); - } - - private postMessage(message: OuterMessage) { + private postMessage(message: OuterMessage, messageId?: number) { // Wrap in try-catch to when the port disconnected without triggering `onDisconnect`. try { const msg: any = message; @@ -261,14 +342,20 @@ export class NativeMessagingBackground { }; } this.port.postMessage(msg); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { - this.logService.error("NativeMessaging port disconnected, disconnecting."); + this.logService.info( + "[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.", + ); this.sharedSecret = null; this.privateKey = null; this.connected = false; - this.rejecter("invalidateEncryption"); + if (this.callbacks.has(messageId)) { + this.callbacks.get(messageId).rejecter("invalidateEncryption"); + } } } @@ -285,90 +372,30 @@ export class NativeMessagingBackground { } if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) { - this.logService.error("NativeMessage is to old, ignoring."); + this.logService.info("[Native Messaging IPC] Received an old native message, ignoring..."); return; } - switch (message.command) { - case "biometricUnlock": { - if ( - ["not available", "not enabled", "not supported", "not unlocked", "canceled"].includes( - message.response, - ) - ) { - this.rejecter(message.response); - return; - } - - // Check for initial setup of biometric unlock - const enabled = await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$); - if (enabled === null || enabled === false) { - if (message.response === "unlocked") { - await this.biometricStateService.setBiometricUnlockEnabled(true); - } - break; - } + const messageId = message.messageId; - // Ignore unlock if already unlocked - if ((await this.authService.getAuthStatus()) === AuthenticationStatus.Unlocked) { - break; - } - - if (message.response === "unlocked") { - try { - if (message.userKeyB64) { - const userKey = new SymmetricCryptoKey( - Utils.fromB64ToArray(message.userKeyB64), - ) as UserKey; - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const isUserKeyValid = await this.keyService.validateUserKey(userKey, activeUserId); - if (isUserKeyValid) { - await this.keyService.setUserKey(userKey, activeUserId); - } else { - this.logService.error("Unable to verify biometric unlocked userkey"); - await this.keyService.clearKeys(activeUserId); - this.rejecter("userkey wrong"); - return; - } - } else { - throw new Error("No key received"); - } - } catch (e) { - this.logService.error("Unable to set key: " + e); - this.rejecter("userkey wrong"); - return; - } - - // Verify key is correct by attempting to decrypt a secret - try { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.keyService.getFingerprint(userId); - } catch (e) { - this.logService.error("Unable to verify key: " + e); - await this.keyService.clearKeys(); - this.rejecter("userkey wrong"); - return; - } - - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.runtimeBackground.processMessage({ command: "unlocked" }); - } - break; - } - case "biometricUnlockAvailable": { - this.resolver(message); - break; - } - default: - this.logService.error("NativeMessage, got unknown command: " + message.command); - break; + if ( + message.command == BiometricsCommands.Unlock || + message.command == BiometricsCommands.IsAvailable + ) { + this.logService.info( + `[Native Messaging IPC] Received legacy message of type ${message.command}`, + ); + const messageId = this.callbacks.keys().next().value; + const resolver = this.callbacks.get(messageId); + this.callbacks.delete(messageId); + resolver.resolver(message); + return; } - if (this.resolver) { - this.resolver(message); + if (this.callbacks.has(messageId)) { + this.callbacks.get(messageId).resolver(message); + } else { + this.logService.info("[Native Messaging IPC] Received message without a callback", message); } } @@ -384,6 +411,7 @@ export class NativeMessagingBackground { command: "setupEncryption", publicKey: Utils.fromBufferToB64(publicKey), userId: userId, + messageId: this.messageId++, }); return new Promise((resolve, reject) => (this.secureSetupResolve = resolve)); diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index 56ad7909e61..38bb2ec50c9 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -13,11 +13,12 @@ import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-managemen import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/messaging"; import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { BiometricsCommands } from "@bitwarden/key-management"; -import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -71,8 +72,10 @@ export default class RuntimeBackground { sendResponse: (response: any) => void, ) => { const messagesWithResponse = [ - "biometricUnlock", - "biometricUnlockAvailable", + BiometricsCommands.AuthenticateWithBiometrics, + BiometricsCommands.GetBiometricsStatus, + BiometricsCommands.UnlockWithBiometricsForUser, + BiometricsCommands.GetBiometricsStatusForUser, "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag", "getInlineMenuFieldQualificationFeatureFlag", "getInlineMenuTotpFeatureFlag", @@ -185,13 +188,17 @@ export default class RuntimeBackground { break; } break; - case "biometricUnlock": { - const result = await this.main.biometricsService.authenticateBiometric(); - return result; + case BiometricsCommands.AuthenticateWithBiometrics: { + return await this.main.biometricsService.authenticateWithBiometrics(); } - case "biometricUnlockAvailable": { - const result = await this.main.biometricsService.isBiometricUnlockAvailable(); - return result; + case BiometricsCommands.GetBiometricsStatus: { + return await this.main.biometricsService.getBiometricsStatus(); + } + case BiometricsCommands.UnlockWithBiometricsForUser: { + return await this.main.biometricsService.unlockWithBiometricsForUser(msg.userId); + } + case BiometricsCommands.GetBiometricsStatusForUser: { + return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId); } case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": { return await this.configService.getFeatureFlag( @@ -202,8 +209,11 @@ export default class RuntimeBackground { return await this.configService.getFeatureFlag(FeatureFlag.InlineMenuFieldQualification); } case "getUserPremiumStatus": { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const result = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); return result; } @@ -249,9 +259,7 @@ export default class RuntimeBackground { await this.main.refreshBadge(); await this.main.refreshMenu(false); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); break; } case "addToLockedVaultPendingNotifications": @@ -278,9 +286,7 @@ export default class RuntimeBackground { await this.configService.ensureConfigFetched(); await this.main.updateOverlayCiphers(); - if (await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh)) { - await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); - } + await this.autofillService.setAutoFillOnPageLoadOrgPolicy(); } break; case "openPopup": diff --git a/apps/browser/src/billing/popup/settings/premium-v2.component.ts b/apps/browser/src/billing/popup/settings/premium-v2.component.ts index c17adcd52fe..f658f71a209 100644 --- a/apps/browser/src/billing/popup/settings/premium-v2.component.ts +++ b/apps/browser/src/billing/popup/settings/premium-v2.component.ts @@ -7,6 +7,7 @@ import { RouterModule } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { PremiumComponent as BasePremiumComponent } from "@bitwarden/angular/vault/components/premium.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -56,6 +57,7 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService: DialogService, environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -66,6 +68,7 @@ export class PremiumV2Component extends BasePremiumComponent { dialogService, environmentService, billingAccountProfileStateService, + accountService, ); // Support old price string. Can be removed in future once all translations are properly updated. diff --git a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts index 0cd48c45938..4c4753c3f7f 100644 --- a/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts +++ b/apps/browser/src/key-management/biometrics/background-browser-biometrics.service.ts @@ -1,36 +1,168 @@ import { Injectable } from "@angular/core"; -import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { + BiometricsService, + BiometricsCommands, + BiometricsStatus, + KeyService, + BiometricStateService, +} from "@bitwarden/key-management"; -import { BrowserBiometricsService } from "./browser-biometrics.service"; +import { NativeMessagingBackground } from "../../background/nativeMessaging.background"; +import { BrowserApi } from "../../platform/browser/browser-api"; @Injectable() -export class BackgroundBrowserBiometricsService extends BrowserBiometricsService { - constructor(private nativeMessagingBackground: () => NativeMessagingBackground) { +export class BackgroundBrowserBiometricsService extends BiometricsService { + constructor( + private nativeMessagingBackground: () => NativeMessagingBackground, + private logService: LogService, + private keyService: KeyService, + private biometricStateService: BiometricStateService, + ) { super(); } - async authenticateBiometric(): Promise { - const responsePromise = this.nativeMessagingBackground().getResponse(); - await this.nativeMessagingBackground().send({ command: "biometricUnlock" }); - const response = await responsePromise; - return response.response === "unlocked"; + async authenticateWithBiometrics(): Promise { + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.Unlock, + }); + return response.response == "unlocked"; + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.AuthenticateWithBiometrics, + }); + return response.response; + } + } catch (e) { + this.logService.info("Biometric authentication failed", e); + return false; + } } - async isBiometricUnlockAvailable(): Promise { - const responsePromise = this.nativeMessagingBackground().getResponse(); - await this.nativeMessagingBackground().send({ command: "biometricUnlockAvailable" }); - const response = await responsePromise; - return response.response === "available"; + async getBiometricsStatus(): Promise { + if (!(await BrowserApi.permissionsGranted(["nativeMessaging"]))) { + return BiometricsStatus.NativeMessagingPermissionMissing; + } + + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.IsAvailable, + }); + const resp = + response.response == "available" + ? BiometricsStatus.Available + : BiometricsStatus.HardwareUnavailable; + return resp; + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.GetBiometricsStatus, + }); + + if (response.response) { + return response.response; + } + } + return BiometricsStatus.Available; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return BiometricsStatus.DesktopDisconnected; + } } - async biometricsNeedsSetup(): Promise { - return false; + async unlockWithBiometricsForUser(userId: UserId): Promise { + try { + await this.ensureConnected(); + + // todo remove after 2025.3 + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.Unlock, + }); + if (response.response == "unlocked") { + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.biometricStateService.setFingerprintValidated(true); + this.keyService.setUserKey(userKey, userId); + return userKey; + } + } else { + return null; + } + } else { + const response = await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.UnlockWithBiometricsForUser, + userId: userId, + }); + if (response.response) { + // In case the requesting foreground context dies (popup), the userkey should still be set, so the user is unlocked / the setting should be enabled + const decodedUserkey = Utils.fromB64ToArray(response.userKeyB64); + const userKey = new SymmetricCryptoKey(decodedUserkey) as UserKey; + if (await this.keyService.validateUserKey(userKey, userId)) { + await this.biometricStateService.setBiometricUnlockEnabled(true); + await this.biometricStateService.setFingerprintValidated(true); + this.keyService.setUserKey(userKey, userId); + return userKey; + } + } else { + return null; + } + } + } catch (e) { + this.logService.info("Biometric unlock for user failed", e); + throw new Error("Biometric unlock failed"); + } + + return null; + } + + async getBiometricsStatusForUser(id: UserId): Promise { + try { + await this.ensureConnected(); + + if (this.nativeMessagingBackground().isConnectedToOutdatedDesktopClient) { + return await this.getBiometricsStatus(); + } + + return ( + await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.GetBiometricsStatusForUser, + userId: id, + }) + ).response; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return BiometricsStatus.DesktopDisconnected; + } + } + + // the first time we call, this might use an outdated version of the protocol, so we drop the response + private async ensureConnected() { + if (!this.nativeMessagingBackground().connected) { + await this.nativeMessagingBackground().callCommand({ + command: BiometricsCommands.IsAvailable, + }); + } } - async biometricsSupportsAutoSetup(): Promise { + async getShouldAutopromptNow(): Promise { return false; } - async biometricsSetup(): Promise {} + async setShouldAutopromptNow(value: boolean): Promise {} } diff --git a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts b/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts deleted file mode 100644 index 7ffbed45415..00000000000 --- a/apps/browser/src/key-management/biometrics/browser-biometrics.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BiometricsService } from "@bitwarden/key-management"; - -import { BrowserApi } from "../../platform/browser/browser-api"; - -@Injectable() -export abstract class BrowserBiometricsService extends BiometricsService { - async supportsBiometric() { - const platformInfo = await BrowserApi.getPlatformInfo(); - if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") { - return true; - } - return false; - } - - abstract authenticateBiometric(): Promise; - abstract isBiometricUnlockAvailable(): Promise; -} diff --git a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts index f50468c8b7a..d248a630cc6 100644 --- a/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts +++ b/apps/browser/src/key-management/biometrics/foreground-browser-biometrics.ts @@ -1,34 +1,55 @@ +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsCommands, BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; + import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserBiometricsService } from "./browser-biometrics.service"; +export class ForegroundBrowserBiometricsService extends BiometricsService { + shouldAutopromptNow = true; -export class ForegroundBrowserBiometricsService extends BrowserBiometricsService { - async authenticateBiometric(): Promise { + async authenticateWithBiometrics(): Promise { const response = await BrowserApi.sendMessageWithResponse<{ result: boolean; error: string; - }>("biometricUnlock"); + }>(BiometricsCommands.AuthenticateWithBiometrics); if (!response.result) { throw response.error; } return response.result; } - async isBiometricUnlockAvailable(): Promise { + async getBiometricsStatus(): Promise { const response = await BrowserApi.sendMessageWithResponse<{ - result: boolean; + result: BiometricsStatus; error: string; - }>("biometricUnlockAvailable"); - return response.result && response.result === true; + }>(BiometricsCommands.GetBiometricsStatus); + return response.result; } - async biometricsNeedsSetup(): Promise { - return false; + async unlockWithBiometricsForUser(userId: UserId): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: UserKey; + error: string; + }>(BiometricsCommands.UnlockWithBiometricsForUser, { userId }); + if (!response.result) { + return null; + } + return SymmetricCryptoKey.fromString(response.result.keyB64) as UserKey; } - async biometricsSupportsAutoSetup(): Promise { - return false; + async getBiometricsStatusForUser(id: UserId): Promise { + const response = await BrowserApi.sendMessageWithResponse<{ + result: BiometricsStatus; + error: string; + }>(BiometricsCommands.GetBiometricsStatusForUser, { userId: id }); + return response.result; } - async biometricsSetup(): Promise {} + async getShouldAutopromptNow(): Promise { + return this.shouldAutopromptNow; + } + async setShouldAutopromptNow(value: boolean): Promise { + this.shouldAutopromptNow = value; + } } diff --git a/apps/browser/src/key-management/browser-key.service.ts b/apps/browser/src/key-management/browser-key.service.ts deleted file mode 100644 index 0cc5f13a27e..00000000000 --- a/apps/browser/src/key-management/browser-key.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { firstValueFrom } from "rxjs"; - -import { PinServiceAbstraction } from "@bitwarden/auth/common"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; -import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { USER_KEY } from "@bitwarden/common/platform/services/key-state/user-key.state"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; -import { UserKey } from "@bitwarden/common/types/key"; -import { - KdfConfigService, - DefaultKeyService, - BiometricsService, - BiometricStateService, -} from "@bitwarden/key-management"; - -export class BrowserKeyService extends DefaultKeyService { - constructor( - pinService: PinServiceAbstraction, - masterPasswordService: InternalMasterPasswordServiceAbstraction, - keyGenerationService: KeyGenerationService, - cryptoFunctionService: CryptoFunctionService, - encryptService: EncryptService, - platformUtilService: PlatformUtilsService, - logService: LogService, - stateService: StateService, - accountService: AccountService, - stateProvider: StateProvider, - private biometricStateService: BiometricStateService, - private biometricsService: BiometricsService, - kdfConfigService: KdfConfigService, - ) { - super( - pinService, - masterPasswordService, - keyGenerationService, - cryptoFunctionService, - encryptService, - platformUtilService, - logService, - stateService, - accountService, - stateProvider, - kdfConfigService, - ); - } - override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { - if (keySuffix === KeySuffixOptions.Biometric) { - const biometricUnlockPromise = - userId == null - ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) - : this.biometricStateService.getBiometricUnlockEnabled(userId); - return await biometricUnlockPromise; - } - return super.hasUserKeyStored(keySuffix, userId); - } - - /** - * Browser doesn't store biometric keys, so we retrieve them from the desktop and return - * if we successfully saved it into memory as the User Key - * @returns the `UserKey` if the user passes a biometrics prompt, otherwise return `null`. - */ - protected override async getKeyFromStorage( - keySuffix: KeySuffixOptions, - userId?: UserId, - ): Promise { - if (keySuffix === KeySuffixOptions.Biometric) { - const biometricsResult = await this.biometricsService.authenticateBiometric(); - - if (!biometricsResult) { - return null; - } - - const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - } - - return await super.getKeyFromStorage(keySuffix, userId); - } -} diff --git a/apps/browser/src/services/extension-lock-component.service.spec.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts similarity index 82% rename from apps/browser/src/services/extension-lock-component.service.spec.ts rename to apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts index a8a019662ef..92830c35aca 100644 --- a/apps/browser/src/services/extension-lock-component.service.spec.ts +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.spec.ts @@ -2,7 +2,6 @@ import { TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; import { PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, @@ -10,9 +9,15 @@ import { import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { KeyService, BiometricsService } from "@bitwarden/key-management"; +import { + KeyService, + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; +import { UnlockOptions } from "@bitwarden/key-management/angular"; -import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; +import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; import { ExtensionLockComponentService } from "./extension-lock-component.service"; @@ -26,6 +31,7 @@ describe("ExtensionLockComponentService", () => { let vaultTimeoutSettingsService: MockProxy; let keyService: MockProxy; let routerService: MockProxy; + let biometricStateService: MockProxy; beforeEach(() => { userDecryptionOptionsService = mock(); @@ -35,6 +41,7 @@ describe("ExtensionLockComponentService", () => { vaultTimeoutSettingsService = mock(); keyService = mock(); routerService = mock(); + biometricStateService = mock(); TestBed.configureTestingModule({ providers: [ @@ -67,6 +74,10 @@ describe("ExtensionLockComponentService", () => { provide: BrowserRouterService, useValue: routerService, }, + { + provide: BiometricStateService, + useValue: biometricStateService, + }, ], }); @@ -121,8 +132,7 @@ describe("ExtensionLockComponentService", () => { describe("getAvailableUnlockOptions$", () => { interface MockInputs { hasMasterPassword: boolean; - osSupportsBiometric: boolean; - biometricLockSet: boolean; + biometricsStatusForUser: BiometricsStatus; hasBiometricEncryptedUserKeyStored: boolean; platformSupportsSecureStorage: boolean; pinDecryptionAvailable: boolean; @@ -133,8 +143,7 @@ describe("ExtensionLockComponentService", () => { // MP + PIN + Biometrics available { hasMasterPassword: true, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: true, @@ -148,7 +157,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -156,8 +165,7 @@ describe("ExtensionLockComponentService", () => { // PIN + Biometrics available { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: true, @@ -171,7 +179,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -179,8 +187,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics available: user key stored with no secure storage { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: false, pinDecryptionAvailable: false, @@ -194,7 +201,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -202,8 +209,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics available: no user key stored with no secure storage { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.Available, hasBiometricEncryptedUserKeyStored: false, platformSupportsSecureStorage: false, pinDecryptionAvailable: false, @@ -217,7 +223,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: true, - disableReason: null, + biometricsStatus: BiometricsStatus.Available, }, }, ], @@ -225,8 +231,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: biometric lock not set { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: false, + biometricsStatusForUser: BiometricsStatus.UnlockNeeded, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -240,7 +245,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + biometricsStatus: BiometricsStatus.UnlockNeeded, }, }, ], @@ -248,8 +253,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: user key not stored { hasMasterPassword: false, - osSupportsBiometric: true, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.NotEnabledInConnectedDesktopApp, hasBiometricEncryptedUserKeyStored: false, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -263,7 +267,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + biometricsStatus: BiometricsStatus.NotEnabledInConnectedDesktopApp, }, }, ], @@ -271,8 +275,7 @@ describe("ExtensionLockComponentService", () => { // Biometrics not available: OS doesn't support { hasMasterPassword: false, - osSupportsBiometric: false, - biometricLockSet: true, + biometricsStatusForUser: BiometricsStatus.HardwareUnavailable, hasBiometricEncryptedUserKeyStored: true, platformSupportsSecureStorage: true, pinDecryptionAvailable: false, @@ -286,7 +289,7 @@ describe("ExtensionLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + biometricsStatus: BiometricsStatus.HardwareUnavailable, }, }, ], @@ -304,12 +307,17 @@ describe("ExtensionLockComponentService", () => { ); // Biometrics - biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + biometricsService.getBiometricsStatusForUser.mockResolvedValue( + mockInputs.biometricsStatusForUser, + ); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); keyService.hasUserKeyStored.mockResolvedValue(mockInputs.hasBiometricEncryptedUserKeyStored); platformUtilsService.supportsSecureStorage.mockReturnValue( mockInputs.platformSupportsSecureStorage, ); + biometricStateService.biometricUnlockEnabled$ = of(true); // PIN pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); diff --git a/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts new file mode 100644 index 00000000000..711ae4e378f --- /dev/null +++ b/apps/browser/src/key-management/lock/services/extension-lock-component.service.ts @@ -0,0 +1,88 @@ +// FIXME: Update this file to be type safe and remove this and next line +// @ts-strict-ignore +import { inject } from "@angular/core"; +import { combineLatest, defer, firstValueFrom, map, Observable } from "rxjs"; + +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; +import { + BiometricsService, + BiometricsStatus, + BiometricStateService, +} from "@bitwarden/key-management"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; + +import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors"; +import { BrowserRouterService } from "../../../platform/popup/services/browser-router.service"; + +export class ExtensionLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly routerService = inject(BrowserRouterService); + private readonly biometricStateService = inject(BiometricStateService); + + getPreviousUrl(): string | null { + return this.routerService.getPreviousUrl(); + } + + getBiometricsError(error: any): string | null { + const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; + + if (!biometricsError) { + return null; + } + + return biometricsError.description; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + return "unlockWithBiometrics"; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(async () => { + if (!(await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$))) { + return BiometricsStatus.NotEnabledLocally; + } else { + // TODO remove after 2025.3 + // remove after backward compatibility code for old biometrics ipc protocol is removed + const result: BiometricsStatus = (await Promise.race([ + this.biometricsService.getBiometricsStatusForUser(userId), + new Promise((resolve) => + setTimeout(() => resolve(BiometricsStatus.DesktopDisconnected), 1000), + ), + ])) as BiometricsStatus; + return result; + } + }), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map(([biometricsStatus, userDecryptionOptions, pinDecryptionAvailable]) => { + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: biometricsStatus === BiometricsStatus.Available, + biometricsStatus: biometricsStatus, + }, + }; + return unlockOpts; + }), + ); + } +} diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 9e4eb78291a..86ea0eebbc8 100644 --- a/apps/browser/src/manifest.json +++ b/apps/browser/src/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.0", + "version": "2025.1.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index ef7ede6b056..ae7c888eb9b 100644 --- a/apps/browser/src/manifest.v3.json +++ b/apps/browser/src/manifest.v3.json @@ -3,7 +3,7 @@ "minimum_chrome_version": "102.0", "name": "__MSG_extName__", "short_name": "__MSG_appName__", - "version": "2025.1.0", + "version": "2025.1.1", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 383e982f065..2b1040bcd8a 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -11,13 +11,11 @@ import { GroupPolicyEnvironment } from "../admin-console/types/group-policy-envi import { BrowserApi } from "./browser/browser-api"; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type Flags = { accountSwitching?: boolean; } & SharedFlags; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts index 4065f2e46d7..67fa920d18d 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -8,6 +8,8 @@ describe("OffscreenDocument", () => { const browserClipboardServiceReadSpy = jest.spyOn(BrowserClipboardService, "read"); const consoleErrorSpy = jest.spyOn(console, "error"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("../offscreen-document/offscreen-document"); describe("init", () => { diff --git a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts index 0919de46776..b177497305b 100644 --- a/apps/browser/src/platform/services/browser-script-injector.service.spec.ts +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -63,7 +63,7 @@ describe("ScriptInjectorService", () => { configService.getFeatureFlag$.mockImplementation(() => of(false)); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); - domainSettingsService.blockedInteractionsUris$ = of(null); + domainSettingsService.blockedInteractionsUris$ = of({}); scriptInjectorService = new BrowserScriptInjectorService( domainSettingsService, platformUtilsService, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts index 949ecebde8a..4d6a403a18a 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.spec.ts @@ -48,6 +48,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, @@ -69,6 +71,8 @@ describe("LocalBackedSessionStorage", () => { localStorage.internalStore["session_test"] = encrypted.encryptedString; encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); const result = await sut.get("test"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect(encryptService.decryptToUtf8).toHaveBeenCalledWith( encrypted, sessionKey, diff --git a/apps/browser/src/platform/services/local-backed-session-storage.service.ts b/apps/browser/src/platform/services/local-backed-session-storage.service.ts index bdc0bed80c0..900212ddefa 100644 --- a/apps/browser/src/platform/services/local-backed-session-storage.service.ts +++ b/apps/browser/src/platform/services/local-backed-session-storage.service.ts @@ -198,6 +198,8 @@ export class LocalBackedSessionStorageService private compareValues(value1: T, value2: T): boolean { try { return compareValues(value1, value2); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.logService.error( `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, diff --git a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts index a2f0c78cd52..0499f34a4ae 100644 --- a/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts +++ b/apps/browser/src/platform/services/sdk/browser-sdk-client-factory.ts @@ -17,6 +17,8 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore } diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 2d53ae7e239..d55bebfa0c3 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -17,7 +17,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; -import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; +import { twofactorRefactorSwap } from "@bitwarden/angular/utils/two-factor-component-refactor-route-swap"; import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards"; import { AnonLayoutWrapperComponent, @@ -25,7 +25,6 @@ import { LoginComponent, LoginSecondaryContentComponent, LockIcon, - LockComponent, LoginViaAuthRequestComponent, PasswordHintComponent, RegistrationFinishComponent, @@ -43,13 +42,13 @@ import { TwoFactorTimeoutIcon, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, VaultIcons, } from "@bitwarden/vault"; -import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; @@ -71,14 +70,10 @@ import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component" import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-domains.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; @@ -148,11 +143,12 @@ const routes: Routes = [ canActivate: [unauthGuardFn(unauthRouteOverrides), unauthUiRefreshRedirect("/login")], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(Fido2V1Component, Fido2Component, { + { path: "fido2", + component: Fido2Component, canActivate: [fido2AuthGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, ...twofactorRefactorSwap( TwoFactorComponent, AnonLayoutWrapperComponent, @@ -321,22 +317,24 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(AutofillV1Component, AutofillComponent, { + { path: "autofill", + component: AutofillComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "account-security", component: AccountSecurityComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, { + { path: "notifications", + component: NotificationsSettingsComponent, canActivate: [authGuard], data: { elevation: 1 } satisfies RouteDataProperties, - }), + }, { path: "vault-settings", component: VaultSettingsV2Component, @@ -355,11 +353,12 @@ const routes: Routes = [ canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, }, - ...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, { + { path: "excluded-domains", + component: ExcludedDomainsComponent, canActivate: [authGuard], data: { elevation: 2 } satisfies RouteDataProperties, - }), + }, { path: "premium", component: PremiumV2Component, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 8e51152be2e..7b6e402a90f 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -21,6 +21,7 @@ import { ToastOptions, ToastService, } from "@bitwarden/components"; +import { BiometricsService, BiometricStateService } from "@bitwarden/key-management"; import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service"; import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service"; @@ -64,6 +65,8 @@ export class AppComponent implements OnInit, OnDestroy { private toastService: ToastService, private accountService: AccountService, private animationControlService: AnimationControlService, + private biometricStateService: BiometricStateService, + private biometricsService: BiometricsService, ) {} async ngOnInit() { @@ -100,7 +103,7 @@ export class AppComponent implements OnInit, OnDestroy { this.messageListener.allMessages$ .pipe( - tap((msg: any) => { + tap(async (msg: any) => { if (msg.command === "doneLoggingOut") { // TODO: PM-8544 - why do we call logout in the popup after receiving the doneLoggingOut message? Hasn't this already completeted logout? this.authService.logOut(async () => { @@ -117,6 +120,7 @@ export class AppComponent implements OnInit, OnDestroy { msg.command === "locked" && (msg.userId == null || msg.userId == this.activeUserId) ) { + await this.biometricsService.setShouldAutopromptNow(false); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["lock"]); @@ -134,6 +138,7 @@ export class AppComponent implements OnInit, OnDestroy { } else if (msg.command === "reloadProcess") { if (this.platformUtilsService.isSafari()) { window.setTimeout(() => { + this.biometricStateService.updateLastProcessReload(); window.location.reload(); }, 2000); } diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index 1fe8f1f18db..15a898aef53 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -35,17 +35,7 @@ import { SsoComponentV1 } from "../auth/popup/sso-v1.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; -import { Fido2CipherRowV1Component } from "../autofill/popup/fido2/fido2-cipher-row-v1.component"; -import { Fido2CipherRowComponent } from "../autofill/popup/fido2/fido2-cipher-row.component"; -import { Fido2UseBrowserLinkV1Component } from "../autofill/popup/fido2/fido2-use-browser-link-v1.component"; -import { Fido2UseBrowserLinkComponent } from "../autofill/popup/fido2/fido2-use-browser-link.component"; -import { Fido2V1Component } from "../autofill/popup/fido2/fido2-v1.component"; -import { Fido2Component } from "../autofill/popup/fido2/fido2.component"; -import { AutofillV1Component } from "../autofill/popup/settings/autofill-v1.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; -import { ExcludedDomainsV1Component } from "../autofill/popup/settings/excluded-domains-v1.component"; -import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component"; -import { NotificationsSettingsV1Component } from "../autofill/popup/settings/notifications-v1.component"; import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component"; import { PopOutComponent } from "../platform/popup/components/pop-out.component"; import { HeaderComponent } from "../platform/popup/header.component"; @@ -87,10 +77,6 @@ import "../platform/popup/locales"; ScrollingModule, ServicesModule, DialogModule, - ExcludedDomainsComponent, - Fido2CipherRowComponent, - Fido2Component, - Fido2UseBrowserLinkComponent, FilePopoutCalloutComponent, AvatarModule, AccountComponent, @@ -112,15 +98,11 @@ import "../platform/popup/locales"; ColorPasswordPipe, ColorPasswordCountPipe, EnvironmentComponent, - ExcludedDomainsV1Component, - Fido2CipherRowV1Component, - Fido2UseBrowserLinkV1Component, HintComponent, HomeComponent, LoginViaAuthRequestComponentV1, LoginComponentV1, LoginDecryptionOptionsComponentV1, - NotificationsSettingsV1Component, RegisterComponent, SetPasswordComponent, SsoComponentV1, @@ -131,8 +113,6 @@ import "../platform/popup/locales"; UserVerificationComponent, VaultTimeoutInputComponent, RemovePasswordComponent, - Fido2V1Component, - AutofillV1Component, EnvironmentSelectorComponent, ], exports: [], diff --git a/apps/browser/src/popup/main.ts b/apps/browser/src/popup/main.ts index 8ffe0743bf6..dadd7917b99 100644 --- a/apps/browser/src/popup/main.ts +++ b/apps/browser/src/popup/main.ts @@ -4,7 +4,9 @@ import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { PopupSizeService } from "../platform/popup/layout/popup-size.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/popup.scss"); +// eslint-disable-next-line @typescript-eslint/no-require-imports require("./scss/tailwind.css"); import { AppModule } from "./app.module"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 92eb8973235..24d82ab8b67 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -24,7 +24,6 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { AnonLayoutWrapperDataService, LoginComponentService, - LockComponentService, SsoComponentService, LoginDecryptionOptionsService, } from "@bitwarden/auth/angular"; @@ -112,9 +111,10 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac import { KdfConfigService, KeyService, - BiometricStateService, BiometricsService, + DefaultKeyService, } from "@bitwarden/key-management"; +import { LockComponentService } from "@bitwarden/key-management/angular"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ForegroundLockService } from "../../auth/popup/accounts/foreground-lock.service"; @@ -126,7 +126,7 @@ import { AutofillService as AutofillServiceAbstraction } from "../../autofill/se import AutofillService from "../../autofill/services/autofill.service"; import { InlineMenuFieldQualificationService } from "../../autofill/services/inline-menu-field-qualification.service"; import { ForegroundBrowserBiometricsService } from "../../key-management/biometrics/foreground-browser-biometrics"; -import { BrowserKeyService } from "../../key-management/browser-key.service"; +import { ExtensionLockComponentService } from "../../key-management/lock/services/extension-lock-component.service"; import { BrowserApi } from "../../platform/browser/browser-api"; import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; /* eslint-disable no-restricted-imports */ @@ -150,7 +150,6 @@ import { BrowserStorageServiceProvider } from "../../platform/storage/browser-st import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { ForegroundSyncService } from "../../platform/sync/foreground-sync.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; -import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { Fido2UserVerificationService } from "../../vault/services/fido2-user-verification.service"; @@ -232,11 +231,9 @@ const safeProviders: SafeProvider[] = [ stateService: StateService, accountService: AccountServiceAbstraction, stateProvider: StateProvider, - biometricStateService: BiometricStateService, - biometricsService: BiometricsService, kdfConfigService: KdfConfigService, ) => { - const keyService = new BrowserKeyService( + const keyService = new DefaultKeyService( pinService, masterPasswordService, keyGenerationService, @@ -247,8 +244,6 @@ const safeProviders: SafeProvider[] = [ stateService, accountService, stateProvider, - biometricStateService, - biometricsService, kdfConfigService, ); new ContainerService(keyService, encryptService).attachToGlobal(self); @@ -265,8 +260,6 @@ const safeProviders: SafeProvider[] = [ StateService, AccountServiceAbstraction, StateProvider, - BiometricStateService, - BiometricsService, KdfConfigService, ], }), @@ -574,7 +567,7 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: SdkClientFactory, - useFactory: (logService) => + useFactory: (logService: LogService) => flagEnabled("sdk") ? new BrowserSdkClientFactory(logService) : new NoopSdkClientFactory(), deps: [LogService], }), diff --git a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift index 1768ce6b15f..58d95f959be 100644 --- a/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift +++ b/apps/browser/src/safari/safari/SafariWebExtensionHandler.swift @@ -86,8 +86,203 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [response], completionHandler: nil) } return - case "biometricUnlock": + case "authenticateWithBiometrics": + let messageId = message?["messageId"] as? Int + let laContext = LAContext() + guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + break + } + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "authenticate") { (success, error) in + if success { + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ]] + } else { + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "authenticateWithBiometrics", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ]] + } + context.completeRequest(returningItems: [response], completionHandler: nil) + } + return + case "getBiometricsStatus": + let messageId = message?["messageId"] as? Int + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatus", + "response": BiometricsStatus.Available.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil); + break + case "unlockWithBiometricsForUser": + let messageId = message?["messageId"] as? Int + var error: NSError? + let laContext = LAContext() + + laContext.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) + + if let e = error, e.code != kLAErrorBiometryLockout { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + + guard let accessControl = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, [.privateKeyUsage, .userPresence], nil) else { + let messageId = message?["messageId"] as? Int + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": false, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "unlock your vault") { (success, error) in + if success { + guard let userId = message?["userId"] as? String else { + return + } + let passwordName = userId + "_user_biometric" + var passwordLength: UInt32 = 0 + var passwordPtr: UnsafeMutableRawPointer? = nil + + var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil) + if status != errSecSuccess { + let fallbackName = "key" + status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) + } + + if status == errSecSuccess { + let result = NSString(bytes: passwordPtr!, length: Int(passwordLength), encoding: String.Encoding.utf8.rawValue) as String? + SecKeychainItemFreeContent(nil, passwordPtr) + + response.userInfo = [ SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "userKeyB64": result!.replacingOccurrences(of: "\"", with: ""), + "messageId": messageId, + ], + ]] + } else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "biometricUnlock", + "response": true, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } + } + + context.completeRequest(returningItems: [response], completionHandler: nil) + } + return + case "getBiometricsStatusForUser": + let messageId = message?["messageId"] as? Int + let laContext = LAContext() + if !laContext.isBiometricsAvailable() { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.HardwareUnavailable.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + + context.completeRequest(returningItems: [response], completionHandler: nil) + break + } + + guard let userId = message?["userId"] as? String else { + return + } + let passwordName = userId + "_user_biometric" + var passwordLength: UInt32 = 0 + var passwordPtr: UnsafeMutableRawPointer? = nil + + var status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(passwordName.utf8.count), passwordName, &passwordLength, &passwordPtr, nil) + if status != errSecSuccess { + let fallbackName = "key" + status = SecKeychainFindGenericPassword(nil, UInt32(ServiceNameBiometric.utf8.count), ServiceNameBiometric, UInt32(fallbackName.utf8.count), fallbackName, &passwordLength, &passwordPtr, nil) + } + + if status == errSecSuccess { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.Available.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } else { + response.userInfo = [ + SFExtensionMessageKey: [ + "message": [ + "command": "getBiometricsStatusForUser", + "response": BiometricsStatus.NotEnabledInConnectedDesktopApp.rawValue, + "timestamp": Int64(NSDate().timeIntervalSince1970 * 1000), + "messageId": messageId, + ], + ], + ] + } + break + case "biometricUnlock": + var error: NSError? let laContext = LAContext() if(!laContext.isBiometricsAvailable()){ @@ -115,7 +310,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { ] break } - laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Bitwarden Safari Extension") { (success, error) in + laContext.evaluateAccessControl(accessControl, operation: .useKeySign, localizedReason: "Biometric Unlock") { (success, error) in if success { guard let userId = message?["userId"] as? String else { return @@ -157,7 +352,6 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { context.completeRequest(returningItems: [response], completionHandler: nil) } - return case "biometricUnlockAvailable": let laContext = LAContext() @@ -228,3 +422,15 @@ class DownloadFileMessage: Decodable, Encodable { class DownloadFileMessageBlobOptions: Decodable, Encodable { var type: String? } + +enum BiometricsStatus : Int { + case Available = 0 + case UnlockNeeded = 1 + case HardwareUnavailable = 2 + case AutoSetupNeeded = 3 + case ManualSetupNeeded = 4 + case PlatformUnsupported = 5 + case DesktopDisconnected = 6 + case NotEnabledLocally = 7 + case NotEnabledInConnectedDesktopApp = 8 +} diff --git a/apps/browser/src/services/extension-lock-component.service.ts b/apps/browser/src/services/extension-lock-component.service.ts deleted file mode 100644 index 4fc2ef69b21..00000000000 --- a/apps/browser/src/services/extension-lock-component.service.ts +++ /dev/null @@ -1,118 +0,0 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -import { inject } from "@angular/core"; -import { combineLatest, defer, map, Observable } from "rxjs"; - -import { - BiometricsDisableReason, - LockComponentService, - UnlockOptions, -} from "@bitwarden/auth/angular"; -import { - PinServiceAbstraction, - UserDecryptionOptionsServiceAbstraction, -} from "@bitwarden/auth/common"; -import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; -import { UserId } from "@bitwarden/common/types/guid"; -import { KeyService, BiometricsService } from "@bitwarden/key-management"; - -import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors"; -import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; - -export class ExtensionLockComponentService implements LockComponentService { - private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); - private readonly platformUtilsService = inject(PlatformUtilsService); - private readonly biometricsService = inject(BiometricsService); - private readonly pinService = inject(PinServiceAbstraction); - private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); - private readonly keyService = inject(KeyService); - private readonly routerService = inject(BrowserRouterService); - - getPreviousUrl(): string | null { - return this.routerService.getPreviousUrl(); - } - - getBiometricsError(error: any): string | null { - const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; - - if (!biometricsError) { - return null; - } - - return biometricsError.description; - } - - async isWindowVisible(): Promise { - throw new Error("Method not implemented."); - } - - getBiometricsUnlockBtnText(): string { - return "unlockWithBiometrics"; - } - - private async isBiometricLockSet(userId: UserId): Promise { - const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); - const hasBiometricEncryptedUserKeyStored = await this.keyService.hasUserKeyStored( - KeySuffixOptions.Biometric, - userId, - ); - const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); - - return ( - biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) - ); - } - - private getBiometricsDisabledReason( - osSupportsBiometric: boolean, - biometricLockSet: boolean, - ): BiometricsDisableReason | null { - if (!osSupportsBiometric) { - return BiometricsDisableReason.NotSupportedOnOperatingSystem; - } else if (!biometricLockSet) { - return BiometricsDisableReason.EncryptedKeysUnavailable; - } - - return null; - } - - getAvailableUnlockOptions$(userId: UserId): Observable { - return combineLatest([ - // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to - defer(() => this.biometricsService.supportsBiometric()), - defer(() => this.isBiometricLockSet(userId)), - this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), - defer(() => this.pinService.isPinDecryptionAvailable(userId)), - ]).pipe( - map( - ([ - supportsBiometric, - isBiometricsLockSet, - userDecryptionOptions, - pinDecryptionAvailable, - ]) => { - const disableReason = this.getBiometricsDisabledReason( - supportsBiometric, - isBiometricsLockSet, - ); - - const unlockOpts: UnlockOptions = { - masterPassword: { - enabled: userDecryptionOptions.hasMasterPassword, - }, - pin: { - enabled: pinDecryptionAvailable, - }, - biometrics: { - enabled: supportsBiometric && isBiometricsLockSet, - disableReason: disableReason, - }, - }; - return unlockOpts; - }, - ), - ); - } -} diff --git a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts index 6270b3503b5..0d49595d2eb 100644 --- a/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts +++ b/apps/browser/src/services/vault-timeout/foreground-vault-timeout.service.ts @@ -1,8 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/src/abstractions/vault-timeout/vault-timeout.service"; -import { MessagingService } from "@bitwarden/common/src/platform/abstractions/messaging.service"; -import { UserId } from "@bitwarden/common/src/types/guid"; +import { VaultTimeoutService as BaseVaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { UserId } from "@bitwarden/common/types/guid"; export class ForegroundVaultTimeoutService implements BaseVaultTimeoutService { constructor(protected messagingService: MessagingService) {} diff --git a/apps/browser/src/tools/background/fileless-importer.background.spec.ts b/apps/browser/src/tools/background/fileless-importer.background.spec.ts index 429a0e12184..409fac9790f 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -39,7 +39,7 @@ describe("FilelessImporterBackground ", () => { let tabMock: chrome.tabs.Tab; beforeEach(() => { - domainSettingsService.blockedInteractionsUris$ = of(null); + domainSettingsService.blockedInteractionsUris$ = of({}); policyService.policyAppliesToActiveUser$.mockImplementation(() => of(true)); scriptInjectorService = new BrowserScriptInjectorService( domainSettingsService, diff --git a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts index 432754ab91c..21fa44b8d3f 100644 --- a/apps/browser/src/tools/content/lp-fileless-importer.spec.ts +++ b/apps/browser/src/tools/content/lp-fileless-importer.spec.ts @@ -12,6 +12,8 @@ describe("LpFilelessImporter", () => { chrome.runtime.connect = jest.fn(() => portSpy); beforeEach(() => { + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./lp-fileless-importer"); lpFilelessImporter = (globalThis as any).lpFilelessImporter; }); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts index 95b49ea00ec..8479235cc17 100644 --- a/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts +++ b/apps/browser/src/tools/content/lp-suppress-import-download-script-append.mv2.spec.ts @@ -7,6 +7,8 @@ describe("LP Suppress Import Download for Manifest v2", () => { return node; }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./lp-suppress-import-download-script-append.mv2"); expect(window.document.createElement).toHaveBeenCalledWith("script"); diff --git a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts index bfff3787506..ff0ed381599 100644 --- a/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts +++ b/apps/browser/src/tools/content/lp-suppress-import-download.spec.ts @@ -10,6 +10,8 @@ describe("LP Suppress Import Download", () => { jest.spyOn(Element.prototype, "appendChild"); jest.spyOn(window, "addEventListener"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-require-imports require("./lp-suppress-import-download"); anchor = document.createElement("a"); diff --git a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts index 506d7146dd6..c3f4634a6c2 100644 --- a/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-v2.component.spec.ts @@ -91,7 +91,17 @@ describe("SendV2Component", () => { CurrentAccountComponent, ], providers: [ - { provide: AccountService, useValue: mock() }, + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "123", + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }, + }, { provide: AuthService, useValue: mock() }, { provide: AvatarService, useValue: mock() }, { diff --git a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts index 2d451dddaa7..8b880e88671 100644 --- a/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts +++ b/apps/browser/src/tools/popup/settings/about-page/more-from-bitwarden-page-v2.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { Observable, firstValueFrom } from "rxjs"; +import { Observable, firstValueFrom, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { DialogService, ItemModule } from "@bitwarden/components"; @@ -36,12 +37,19 @@ export class MoreFromBitwardenPageV2Component { constructor( private dialogService: DialogService, - billingAccountProfileStateService: BillingAccountProfileStateService, + private billingAccountProfileStateService: BillingAccountProfileStateService, private environmentService: EnvironmentService, private organizationService: OrganizationService, private familiesPolicyService: FamiliesPolicyService, + private accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + account + ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + ); this.familySponsorshipAvailable$ = this.organizationService.familySponsorshipAvailable$; this.hasSingleEnterpriseOrg$ = this.familiesPolicyService.hasSingleEnterpriseOrg$(); this.isFreeFamilyPolicyEnabled$ = this.familiesPolicyService.isFreeFamilyPolicyEnabled$(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts index 8c1e0641b03..4f6c4aa07cf 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -1,7 +1,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { Router } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -10,7 +10,6 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -55,7 +54,14 @@ describe("OpenAttachmentsComponent", () => { const showFilePopoutMessage = jest.fn().mockReturnValue(false); const mockUserId = Utils.newGuid() as UserId; - const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const accountService = { + activeAccount$: of({ + id: mockUserId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }), + }; beforeEach(async () => { openCurrentPagePopout.mockClear(); @@ -63,6 +69,7 @@ describe("OpenAttachmentsComponent", () => { showToast.mockClear(); getOrganization.mockClear(); showFilePopoutMessage.mockClear(); + hasPremiumFromAnySource$.next(true); await TestBed.configureTestingModule({ imports: [OpenAttachmentsComponent, RouterTestingModule], @@ -96,7 +103,7 @@ describe("OpenAttachmentsComponent", () => { }).compileComponents(); }); - beforeEach(() => { + beforeEach(async () => { fixture = TestBed.createComponent(OpenAttachmentsComponent); component = fixture.componentInstance; component.cipherId = "5555-444-3333" as CipherId; @@ -107,7 +114,7 @@ describe("OpenAttachmentsComponent", () => { it("opens attachments in new popout", async () => { showFilePopoutMessage.mockReturnValue(true); - + component.canAccessAttachments = true; await component.ngOnInit(); await component.openAttachments(); @@ -120,7 +127,7 @@ describe("OpenAttachmentsComponent", () => { it("opens attachments in same window", async () => { showFilePopoutMessage.mockReturnValue(false); - + component.canAccessAttachments = true; await component.ngOnInit(); await component.openAttachments(); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index ca620531ca8..5e27ccd5c41 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -4,7 +4,7 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { Router } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom, map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -54,8 +54,13 @@ export class OpenAttachmentsComponent implements OnInit { private filePopoutUtilsService: FilePopoutUtilsService, private accountService: AccountService, ) { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntilDestroyed()) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntilDestroyed(), + ) .subscribe((canAccessPremium) => { this.canAccessAttachments = canAccessPremium; }); diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html index 7c4ea3e5b46..047d168ecbb 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.html @@ -1,7 +1,7 @@ + {{ "autofillBlockedNoticeV2" | i18n }} + + {{ "autofillBlockedNoticeGuidance" | i18n }} + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts new file mode 100644 index 00000000000..3a17825f4fb --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/blocked-injection-banner/blocked-injection-banner.component.ts @@ -0,0 +1,53 @@ +import { CommonModule } from "@angular/common"; +import { Component, OnInit } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Observable } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + BannerModule, + IconButtonModule, + LinkModule, + TypographyModule, +} from "@bitwarden/components"; + +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; + +const blockedURISettingsRoute = "/blocked-domains"; + +@Component({ + standalone: true, + imports: [ + BannerModule, + CommonModule, + IconButtonModule, + JslibModule, + LinkModule, + RouterModule, + TypographyModule, + ], + selector: "blocked-injection-banner", + templateUrl: "blocked-injection-banner.component.html", +}) +export class BlockedInjectionBanner implements OnInit { + /** + * Flag indicating that the banner should be shown + */ + protected showCurrentTabIsBlockedBanner$: Observable = + this.vaultPopupAutofillService.showCurrentTabIsBlockedBanner$; + + /** + * Hostname for current tab + */ + protected currentTabHostname?: string; + + blockedURISettingsRoute: string = blockedURISettingsRoute; + + constructor(private vaultPopupAutofillService: VaultPopupAutofillService) {} + + async ngOnInit() {} + + async handleCurrentTabIsBlockedBannerDismiss() { + await this.vaultPopupAutofillService.dismissCurrentTabIsBlockedBanner(); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index 7f87f32fcd4..4c7067df53a 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -5,6 +5,7 @@ size="small" [attr.aria-label]="'moreOptionsLabel' | i18n: cipher.name" [title]="'moreOptionsTitle' | i18n: cipher.name" + [disabled]="cipher.decryptionFailure" [bitMenuTriggerFor]="moreOptions" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html index 7b31e647bf0..4d617ff7786 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.html @@ -27,6 +27,15 @@ {{ "note" | i18n }} + + + {{ "typeSshKey" | i18n }} + - {{ ciphers.length }} - - + + + + + + + + +
+ +
+ + +
+ + + + +

+ {{ title }} +

+ + + + {{ ciphers.length }} + + + + + +
+
+ +
{{ description }}
+
+ + - + @@ -27,7 +42,12 @@

[bitMenuTriggerFor]="moreOptions" > - -
diff --git a/apps/desktop/src/vault/app/vault/add-edit.component.ts b/apps/desktop/src/vault/app/vault/add-edit.component.ts index a798e61aa88..02fa8076086 100644 --- a/apps/desktop/src/vault/app/vault/add-edit.component.ts +++ b/apps/desktop/src/vault/app/vault/add-edit.component.ts @@ -19,9 +19,9 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService, ToastService } from "@bitwarden/components"; import { SshKeyPasswordPromptComponent } from "@bitwarden/importer/ui"; @@ -56,8 +56,9 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On dialogService: DialogService, datePipe: DatePipe, configService: ConfigService, - private toastService: ToastService, + toastService: ToastService, cipherAuthorizationService: CipherAuthorizationService, + sdkService: SdkService, ) { super( cipherService, @@ -78,6 +79,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On datePipe, configService, cipherAuthorizationService, + toastService, + sdkService, ); } @@ -114,17 +117,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On } await super.load(); - - if (!this.editMode || this.cloneMode) { - // Creating an ssh key directly while filtering to the ssh key category - // must force a key to be set. SSH keys must never be created with an empty private key field - if ( - this.cipher.type === CipherType.SshKey && - (this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "") - ) { - await this.generateSshKey(false); - } - } } onWindowHidden() { @@ -156,21 +148,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On ); } - async generateSshKey(showNotification: boolean = true) { - const sshKey = await ipc.platform.sshAgent.generateKey("ed25519"); - this.cipher.sshKey.privateKey = sshKey.privateKey; - this.cipher.sshKey.publicKey = sshKey.publicKey; - this.cipher.sshKey.keyFingerprint = sshKey.keyFingerprint; - - if (showNotification) { - this.toastService.showToast({ - variant: "success", - title: "", - message: this.i18nService.t("sshKeyGenerated"), - }); - } - } - async importSshKeyFromClipboard(password: string = "") { const key = await this.platformUtilsService.readFromClipboard(); const parsedKey = await ipc.platform.sshAgent.importKey(key, password); @@ -234,12 +211,6 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On return await lastValueFrom(dialog.closed); } - async typeChange() { - if (this.cipher.type === CipherType.SshKey) { - await this.generateSshKey(); - } - } - truncateString(value: string, length: number) { return value.length > length ? value.substring(0, length) + "..." : value; } diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html index a294ce1f63f..d16f8e4f1cb 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.html @@ -4,6 +4,7 @@ -
diff --git a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts index abf74371912..ae6f031005e 100644 --- a/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts +++ b/apps/desktop/src/vault/app/vault/credential-generator-dialog.component.ts @@ -14,6 +14,7 @@ import { CredentialGeneratorHistoryDialogComponent, GeneratorModule, } from "@bitwarden/generator-components"; +import { AlgorithmInfo } from "@bitwarden/generator-core"; import { CipherFormGeneratorComponent } from "@bitwarden/vault"; type CredentialGeneratorParams = { @@ -38,12 +39,17 @@ type CredentialGeneratorParams = { }) export class CredentialGeneratorDialogComponent { credentialValue?: string; + buttonLabel?: string; constructor( @Inject(DIALOG_DATA) protected data: CredentialGeneratorParams, private dialogService: DialogService, ) {} + algorithm = (selected: AlgorithmInfo) => { + this.buttonLabel = selected.useGeneratedValue; + }; + applyCredentials = () => { this.data.onCredentialGenerated(this.credentialValue); }; diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index f375b303024..c2260692fbd 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -10,14 +10,15 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { Subject, takeUntil } from "rxjs"; -import { first } from "rxjs/operators"; +import { combineLatest, firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { filter, first, map, take } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -27,13 +28,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { DialogService } from "@bitwarden/components"; -import { PasswordRepromptService } from "@bitwarden/vault"; +import { DecryptionFailureDialogComponent, PasswordRepromptService } from "@bitwarden/vault"; import { SearchBarService } from "../../../app/layout/search/search-bar.service"; import { GeneratorComponent } from "../../../app/tools/generator.component"; @@ -111,11 +114,18 @@ export class VaultComponent implements OnInit, OnDestroy { private dialogService: DialogService, private billingAccountProfileStateService: BillingAccountProfileStateService, private configService: ConfigService, + private accountService: AccountService, + private cipherService: CipherService, ) {} async ngOnInit() { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.componentIsDestroyed$)) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.componentIsDestroyed$), + ) .subscribe((canAccessPremium: boolean) => { this.userHasPremiumAccess = canAccessPremium; }); @@ -231,6 +241,25 @@ export class VaultComponent implements OnInit, OnDestroy { notificationId: authRequest.id, }); } + + // Store a reference to the current active account during page init + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + // Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit. + // The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions + combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$]) + .pipe( + filter(([account]) => account.id === activeAccount.id), + map(([_, ciphers]) => ciphers.filter((c) => !c.isDeleted)), + filter((ciphers) => ciphers.length > 0), + take(1), + takeUntil(this.componentIsDestroyed$), + ) + .subscribe((ciphers) => { + DecryptionFailureDialogComponent.open(this.dialogService, { + cipherIds: ciphers.map((c) => c.id as CipherId), + }); + }); } ngOnDestroy() { @@ -295,6 +324,12 @@ export class VaultComponent implements OnInit, OnDestroy { }), }, ]; + + if (cipher.decryptionFailure) { + invokeMenu(menu); + return; + } + if (!cipher.isDeleted) { menu.push({ label: this.i18nService.t("edit"), diff --git a/apps/desktop/src/vault/app/vault/view.component.html b/apps/desktop/src/vault/app/vault/view.component.html index e6c20d2e89e..f589ba53046 100644 --- a/apps/desktop/src/vault/app/vault/view.component.html +++ b/apps/desktop/src/vault/app/vault/view.component.html @@ -186,6 +186,16 @@

+
+
+ {{ "verificationCodeTotp" | i18n }} + + {{ "organizationUpgradeRequired" | i18n }} + + +
+
@@ -638,33 +648,35 @@

+ +

{{ "deviceListDescriptionTemp" | i18n }}

+ +
+ +
+ + + + + {{ col.title }} + + + + + +
+ +
+
+ + + {{ row.displayName }} + + + + {{ "needsApproval" | i18n }} + + + + {{ row.displayName }} + + {{ "trusted" | i18n }} + + +
+ + + {{ + "currentSession" | i18n + }} + {{ + "requestPending" | i18n + }} + + {{ row.firstLogin | date: "medium" }} + +
+
+ diff --git a/apps/web/src/app/auth/settings/security/device-management.component.ts b/apps/web/src/app/auth/settings/security/device-management.component.ts new file mode 100644 index 00000000000..e22122ad9ae --- /dev/null +++ b/apps/web/src/app/auth/settings/security/device-management.component.ts @@ -0,0 +1,243 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { combineLatest, firstValueFrom } from "rxjs"; + +import { LoginApprovalComponent } from "@bitwarden/auth/angular"; +import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; +import { + DevicePendingAuthRequest, + DeviceResponse, +} from "@bitwarden/common/auth/abstractions/devices/responses/device.response"; +import { DeviceView } from "@bitwarden/common/auth/abstractions/devices/views/device.view"; +import { DeviceType, DeviceTypeMetadata } from "@bitwarden/common/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { + DialogService, + ToastService, + TableDataSource, + TableModule, + PopoverModule, +} from "@bitwarden/components"; + +import { SharedModule } from "../../../shared"; + +interface DeviceTableData { + id: string; + type: DeviceType; + displayName: string; + loginStatus: string; + firstLogin: Date; + trusted: boolean; + devicePendingAuthRequest: DevicePendingAuthRequest | null; + hasPendingAuthRequest: boolean; +} + +/** + * Provides a table of devices and allows the user to log out, approve or remove a device + */ +@Component({ + selector: "app-device-management", + templateUrl: "./device-management.component.html", + standalone: true, + imports: [CommonModule, SharedModule, TableModule, PopoverModule], +}) +export class DeviceManagementComponent { + protected readonly tableId = "device-management-table"; + protected dataSource = new TableDataSource(); + protected currentDevice: DeviceView | undefined; + protected loading = true; + protected asyncActionLoading = false; + + constructor( + private i18nService: I18nService, + private devicesService: DevicesServiceAbstraction, + private dialogService: DialogService, + private toastService: ToastService, + private validationService: ValidationService, + ) { + combineLatest([this.devicesService.getCurrentDevice$(), this.devicesService.getDevices$()]) + .pipe(takeUntilDestroyed()) + .subscribe({ + next: ([currentDevice, devices]: [DeviceResponse, Array]) => { + this.currentDevice = new DeviceView(currentDevice); + + this.dataSource.data = devices.map((device: DeviceView): DeviceTableData => { + return { + id: device.id, + type: device.type, + displayName: this.getHumanReadableDeviceType(device.type), + loginStatus: this.getLoginStatus(device), + firstLogin: new Date(device.creationDate), + trusted: device.response.isTrusted, + devicePendingAuthRequest: device.response.devicePendingAuthRequest, + hasPendingAuthRequest: this.hasPendingAuthRequest(device.response), + }; + }); + + this.loading = false; + }, + error: () => { + this.loading = false; + }, + }); + } + + /** + * Column configuration for the table + */ + protected readonly columnConfig = [ + { + name: "displayName", + title: this.i18nService.t("device"), + headerClass: "tw-w-1/3", + sortable: true, + }, + { + name: "loginStatus", + title: this.i18nService.t("loginStatus"), + headerClass: "tw-w-1/3", + sortable: true, + }, + { + name: "firstLogin", + title: this.i18nService.t("firstLogin"), + headerClass: "tw-w-1/3", + sortable: true, + }, + ]; + + /** + * Get the icon for a device type + * @param type - The device type + * @returns The icon for the device type + */ + getDeviceIcon(type: DeviceType): string { + const defaultIcon = "bwi bwi-desktop"; + const categoryIconMap: Record = { + webVault: "bwi bwi-browser", + desktop: "bwi bwi-desktop", + mobile: "bwi bwi-mobile", + cli: "bwi bwi-cli", + extension: "bwi bwi-puzzle", + sdk: "bwi bwi-desktop", + }; + + const metadata = DeviceTypeMetadata[type]; + return metadata ? (categoryIconMap[metadata.category] ?? defaultIcon) : defaultIcon; + } + + /** + * Get the login status of a device + * It will return the current session if the device is the current device + * It will return the date of the pending auth request when available + * @param device - The device + * @returns The login status + */ + private getLoginStatus(device: DeviceView): string { + if (this.isCurrentDevice(device)) { + return this.i18nService.t("currentSession"); + } + + if (device.response.devicePendingAuthRequest?.creationDate) { + return this.i18nService.t("requestPending"); + } + + return ""; + } + + /** + * Get a human readable device type from the DeviceType enum + * @param type - The device type + * @returns The human readable device type + */ + private getHumanReadableDeviceType(type: DeviceType): string { + const metadata = DeviceTypeMetadata[type]; + if (!metadata) { + return this.i18nService.t("unknownDevice"); + } + + // If the platform is "Unknown" translate it since it is not a proper noun + const platform = + metadata.platform === "Unknown" ? this.i18nService.t("unknown") : metadata.platform; + const category = this.i18nService.t(metadata.category); + return platform ? `${category} - ${platform}` : category; + } + + /** + * Check if a device is the current device + * @param device - The device or device table data + * @returns True if the device is the current device, false otherwise + */ + protected isCurrentDevice(device: DeviceView | DeviceTableData): boolean { + return "response" in device + ? device.id === this.currentDevice?.id + : device.id === this.currentDevice?.id; + } + + /** + * Check if a device has a pending auth request + * @param device - The device response + * @returns True if the device has a pending auth request, false otherwise + */ + private hasPendingAuthRequest(device: DeviceResponse): boolean { + return ( + device.devicePendingAuthRequest !== undefined && device.devicePendingAuthRequest !== null + ); + } + + /** + * Open a dialog to approve or deny a pending auth request for a device + */ + async managePendingAuthRequest(device: DeviceTableData) { + if (device.devicePendingAuthRequest === undefined || device.devicePendingAuthRequest === null) { + return; + } + + const dialogRef = LoginApprovalComponent.open(this.dialogService, { + notificationId: device.devicePendingAuthRequest.id, + }); + + const result = await firstValueFrom(dialogRef.closed); + + if (result !== undefined && typeof result === "boolean") { + // auth request approved or denied so reset + device.devicePendingAuthRequest = null; + device.hasPendingAuthRequest = false; + } + } + + /** + * Remove a device + * @param device - The device + */ + protected async removeDevice(device: DeviceTableData) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "removeDevice" }, + content: { key: "removeDeviceConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + this.asyncActionLoading = true; + await firstValueFrom(this.devicesService.deactivateDevice$(device.id)); + this.asyncActionLoading = false; + + // Remove the device from the data source + this.dataSource.data = this.dataSource.data.filter((d) => d.id !== device.id); + + this.toastService.showToast({ + title: "", + message: this.i18nService.t("deviceRemoved"), + variant: "success", + }); + } catch (error) { + this.validationService.showError(error); + } + } +} diff --git a/apps/web/src/app/auth/settings/security/security-routing.module.ts b/apps/web/src/app/auth/settings/security/security-routing.module.ts index 8af0499d05a..6ed21605184 100644 --- a/apps/web/src/app/auth/settings/security/security-routing.module.ts +++ b/apps/web/src/app/auth/settings/security/security-routing.module.ts @@ -4,6 +4,7 @@ import { RouterModule, Routes } from "@angular/router"; import { ChangePasswordComponent } from "../change-password.component"; import { TwoFactorSetupComponent } from "../two-factor/two-factor-setup.component"; +import { DeviceManagementComponent } from "./device-management.component"; import { SecurityKeysComponent } from "./security-keys.component"; import { SecurityComponent } from "./security.component"; @@ -29,6 +30,11 @@ const routes: Routes = [ component: SecurityKeysComponent, data: { titleId: "keys" }, }, + { + path: "device-management", + component: DeviceManagementComponent, + data: { titleId: "devices" }, + }, ], }, ]; diff --git a/apps/web/src/app/auth/settings/security/security.component.html b/apps/web/src/app/auth/settings/security/security.component.html index 25459faeacc..6bd7c1daf36 100644 --- a/apps/web/src/app/auth/settings/security/security.component.html +++ b/apps/web/src/app/auth/settings/security/security.component.html @@ -4,6 +4,7 @@ {{ "masterPassword" | i18n }} {{ "twoStepLogin" | i18n }} + {{ "devices" | i18n }} {{ "keys" | i18n }} diff --git a/apps/web/src/app/auth/settings/security/security.component.ts b/apps/web/src/app/auth/settings/security/security.component.ts index 1df8145a917..d643b565df2 100644 --- a/apps/web/src/app/auth/settings/security/security.component.ts +++ b/apps/web/src/app/auth/settings/security/security.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @Component({ selector: "app-security", @@ -9,7 +10,10 @@ import { UserVerificationService } from "@bitwarden/common/auth/abstractions/use export class SecurityComponent implements OnInit { showChangePassword = true; - constructor(private userVerificationService: UserVerificationService) {} + constructor( + private userVerificationService: UserVerificationService, + private configService: ConfigService, + ) {} async ngOnInit() { this.showChangePassword = await this.userVerificationService.hasMasterPassword(); diff --git a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts index 14cf63d3f4e..3b20718873d 100644 --- a/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts +++ b/apps/web/src/app/auth/settings/two-factor/two-factor-setup.component.ts @@ -10,6 +10,7 @@ import { Subject, Subscription, takeUntil, + switchMap, } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -18,6 +19,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { TwoFactorAuthenticatorResponse } from "@bitwarden/common/auth/models/response/two-factor-authenticator.response"; import { TwoFactorDuoResponse } from "@bitwarden/common/auth/models/response/two-factor-duo.response"; @@ -69,8 +71,13 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { protected messagingService: MessagingService, protected policyService: PolicyService, billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); } async ngOnInit() { diff --git a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts index 2abab57b7e0..11b55f92b40 100644 --- a/apps/web/src/app/billing/individual/premium/premium-v2.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium-v2.component.ts @@ -4,10 +4,11 @@ import { Component, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, concatMap, from, Observable, of } from "rxjs"; +import { combineLatest, concatMap, from, Observable, of, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; @@ -65,14 +66,22 @@ export class PremiumV2Component { private toastService: ToastService, private tokenService: TokenService, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.isSelfHost = this.platformUtilsService.isSelfHost(); - this.hasPremiumFromAnyOrganization$ = - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$; + this.hasPremiumFromAnyOrganization$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$(account.id), + ), + ); combineLatest([ - this.billingAccountProfileStateService.hasPremiumPersonally$, + this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumPersonally$(account.id), + ), + ), this.environmentService.cloudWebVaultUrl$, ]) .pipe( diff --git a/apps/web/src/app/billing/individual/premium/premium.component.ts b/apps/web/src/app/billing/individual/premium/premium.component.ts index 76ca25c8cc6..f96f573cd4d 100644 --- a/apps/web/src/app/billing/individual/premium/premium.component.ts +++ b/apps/web/src/app/billing/individual/premium/premium.component.ts @@ -4,10 +4,11 @@ import { Component, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl, FormGroup, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { firstValueFrom, Observable } from "rxjs"; +import { firstValueFrom, Observable, switchMap } from "rxjs"; import { debounceTime } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; @@ -58,9 +59,14 @@ export class PremiumComponent implements OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, private taxService: TaxServiceAbstraction, + private accountService: AccountService, ) { this.selfHosted = platformUtilsService.isSelfHost(); - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); this.addonForm.controls.additionalStorage.valueChanges .pipe(debounceTime(1000), takeUntilDestroyed()) @@ -75,7 +81,10 @@ export class PremiumComponent implements OnInit { } async ngOnInit() { this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); - if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { + const account = await firstValueFrom(this.accountService.activeAccount$); + if ( + await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(account.id)) + ) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/settings/subscription/user-subscription"]); diff --git a/apps/web/src/app/billing/individual/subscription.component.ts b/apps/web/src/app/billing/individual/subscription.component.ts index d8d435d8fe5..edd16ca81fe 100644 --- a/apps/web/src/app/billing/individual/subscription.component.ts +++ b/apps/web/src/app/billing/individual/subscription.component.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, switchMap } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,8 +17,11 @@ export class SubscriptionComponent implements OnInit { constructor( private platformUtilsService: PlatformUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { - this.hasPremium$ = billingAccountProfileStateService.hasPremiumPersonally$; + this.hasPremium$ = accountService.activeAccount$.pipe( + switchMap((account) => billingAccountProfileStateService.hasPremiumPersonally$(account.id)), + ); } ngOnInit() { diff --git a/apps/web/src/app/billing/individual/user-subscription.component.ts b/apps/web/src/app/billing/individual/user-subscription.component.ts index 57d5ef314ec..97b4725e6d7 100644 --- a/apps/web/src/app/billing/individual/user-subscription.component.ts +++ b/apps/web/src/app/billing/individual/user-subscription.component.ts @@ -5,6 +5,7 @@ import { Router } from "@angular/router"; import { firstValueFrom, lastValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -60,6 +61,7 @@ export class UserSubscriptionComponent implements OnInit { private billingAccountProfileStateService: BillingAccountProfileStateService, private toastService: ToastService, private configService: ConfigService, + private accountService: AccountService, ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } @@ -75,7 +77,10 @@ export class UserSubscriptionComponent implements OnInit { return; } - if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { + const userId = await firstValueFrom(this.accountService.activeAccount$); + if ( + await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$(userId.id)) + ) { this.loading = true; this.sub = await this.apiService.getUserSubscription(); } else { diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html index 78005275f12..89c0075ba0d 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.html +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.html @@ -1,16 +1,19 @@
- {{ "upgradeFreeOrganization" | i18n: currentPlanName }} + {{ dialogHeaderName }}

{{ "upgradePlans" | i18n }}

- {{ "selectAPlan" | i18n }} + {{ + "selectAPlan" | i18n + }}
+
+

{{ "paymentMethod" | i18n }}

-

+

{{ deprecateStripeSourcesAPI diff --git a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts index 9a80de555c6..bc5c7da8db9 100644 --- a/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts +++ b/apps/web/src/app/billing/organizations/change-plan-dialog.component.ts @@ -24,7 +24,14 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationUpgradeRequest } from "@bitwarden/common/admin-console/models/request/organization-upgrade.request"; -import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions"; +import { + BillingApiServiceAbstraction, + BillingInformation, + OrganizationInformation, + PaymentInformation, + PlanInformation, + OrganizationBillingServiceAbstraction as OrganizationBillingService, +} from "@bitwarden/common/billing/abstractions"; import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; import { PaymentMethodType, @@ -49,6 +56,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; +import { BillingSharedModule } from "../shared/billing-shared.module"; import { PaymentV2Component } from "../shared/payment/payment-v2.component"; import { PaymentComponent } from "../shared/payment/payment.component"; @@ -89,6 +97,8 @@ interface OnSuccessArgs { @Component({ templateUrl: "./change-plan-dialog.component.html", + standalone: true, + imports: [BillingSharedModule], }) export class ChangePlanDialogComponent implements OnInit, OnDestroy { @ViewChild(PaymentComponent) paymentComponent: PaymentComponent; @@ -163,6 +173,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { organization: Organization; sub: OrganizationSubscriptionResponse; billing: BillingResponse; + dialogHeaderName: string; currentPlanName: string; showPayment: boolean = false; totalOpened: boolean = false; @@ -174,6 +185,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { paymentSource?: PaymentSourceResponse; deprecateStripeSourcesAPI: boolean; + isSubscriptionCanceled: boolean = false; private destroy$ = new Subject(); @@ -196,6 +208,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { private configService: ConfigService, private billingApiService: BillingApiServiceAbstraction, private taxService: TaxServiceAbstraction, + private organizationBillingService: OrganizationBillingService, ) {} async ngOnInit(): Promise { @@ -208,6 +221,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.sub = this.dialogParams.subscription ?? (await this.organizationApiService.getSubscription(this.dialogParams.organizationId)); + this.dialogHeaderName = this.resolveHeaderName(this.sub); this.organizationId = this.dialogParams.organizationId; this.currentPlan = this.sub?.plan; this.selectedPlan = this.sub?.plan; @@ -281,9 +295,25 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.refreshSalesTax(); } + resolveHeaderName(subscription: OrganizationSubscriptionResponse): string { + if (subscription.subscription != null) { + this.isSubscriptionCanceled = subscription.subscription.cancelled; + if (subscription.subscription.cancelled) { + return this.i18nService.t("restartSubscription"); + } + } + + return this.i18nService.t( + "upgradeFreeOrganization", + this.resolvePlanName(this.dialogParams.productTierType), + ); + } + setInitialPlanSelection() { this.focusedIndex = this.selectableProducts.length - 1; - this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + if (!this.isSubscriptionCanceled) { + this.selectPlan(this.getPlanByType(ProductTierType.Enterprise)); + } } getPlanByType(productTier: ProductTierType) { @@ -388,6 +418,19 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { ]; } case PlanCardState.Disabled: { + if (this.isSubscriptionCanceled) { + return [ + "tw-cursor-not-allowed", + "tw-bg-secondary-100", + "tw-font-normal", + "tw-bg-blur", + "tw-text-muted", + "tw-block", + "tw-rounded", + "tw-w-80", + ]; + } + return [ "tw-cursor-not-allowed", "tw-bg-secondary-100", @@ -409,7 +452,7 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return; } - if (plan === this.currentPlan) { + if (plan === this.currentPlan && !this.isSubscriptionCanceled) { return; } this.selectedPlan = plan; @@ -446,6 +489,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } get selectableProducts() { + if (this.isSubscriptionCanceled) { + // Return only the current plan if the subscription is canceled + return [this.currentPlan]; + } + if (this.acceptingSponsorship) { const familyPlan = this.passwordManagerPlans.find( (plan) => plan.type === PlanType.FamiliesAnnually, @@ -692,11 +740,18 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { const doSubmit = async (): Promise => { let orgId: string = null; - orgId = await this.updateOrganization(); + if (this.isSubscriptionCanceled) { + await this.restartSubscription(); + orgId = this.organizationId; + } else { + orgId = await this.updateOrganization(); + } this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t("organizationUpgraded"), + message: this.isSubscriptionCanceled + ? this.i18nService.t("restartOrganizationSubscription") + : this.i18nService.t("organizationUpgraded"), }); await this.apiService.refreshIdentityToken(); @@ -726,6 +781,44 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { this.dialogRef.close(); }; + private async restartSubscription() { + const org = await this.organizationApiService.get(this.organizationId); + const organization: OrganizationInformation = { + name: org.name, + billingEmail: org.billingEmail, + }; + + const plan: PlanInformation = { + type: this.selectedPlan.type, + passwordManagerSeats: org.seats, + }; + + if (org.useSecretsManager) { + plan.subscribeToSecretsManager = true; + plan.secretsManagerSeats = org.smSeats; + } + + let paymentMethod: [string, PaymentMethodType]; + + if (this.deprecateStripeSourcesAPI) { + const { type, token } = await this.paymentV2Component.tokenize(); + paymentMethod = [token, type]; + } else { + paymentMethod = await this.paymentComponent.createPaymentToken(); + } + + const payment: PaymentInformation = { + paymentMethod, + billing: this.getBillingInformationFromTaxInfoComponent(), + }; + + await this.organizationBillingService.restartSubscription(this.organization.id, { + organization, + plan, + payment, + }); + } + private async updateOrganization() { const request = new OrganizationUpgradeRequest(); if (this.selectedPlan.productTier !== ProductTierType.Families) { @@ -802,6 +895,18 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { return text; } + private getBillingInformationFromTaxInfoComponent(): BillingInformation { + return { + country: this.taxInformation.country, + postalCode: this.taxInformation.postalCode, + taxId: this.taxInformation.taxId, + addressLine1: this.taxInformation.line1, + addressLine2: this.taxInformation.line2, + city: this.taxInformation.city, + state: this.taxInformation.state, + }; + } + private buildSecretsManagerRequest(request: OrganizationUpgradeRequest): void { request.useSecretsManager = this.organization.useSecretsManager; if (!this.organization.useSecretsManager) { @@ -997,6 +1102,11 @@ export class ChangePlanDialogComponent implements OnInit, OnDestroy { } protected canUpdatePaymentInformation(): boolean { - return this.upgradeRequiresPaymentMethod || this.showPayment || this.isPaymentSourceEmpty(); + return ( + this.upgradeRequiresPaymentMethod || + this.showPayment || + this.isPaymentSourceEmpty() || + this.isSubscriptionCanceled + ); } } diff --git a/apps/web/src/app/billing/organizations/organization-billing.module.ts b/apps/web/src/app/billing/organizations/organization-billing.module.ts index b25cda662f2..48ac613711d 100644 --- a/apps/web/src/app/billing/organizations/organization-billing.module.ts +++ b/apps/web/src/app/billing/organizations/organization-billing.module.ts @@ -8,7 +8,6 @@ import { BillingSharedModule } from "../shared"; import { AdjustSubscription } from "./adjust-subscription.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; import { BillingSyncKeyComponent } from "./billing-sync-key.component"; -import { ChangePlanDialogComponent } from "./change-plan-dialog.component"; import { ChangePlanComponent } from "./change-plan.component"; import { DownloadLicenceDialogComponent } from "./download-license.component"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; @@ -44,7 +43,6 @@ import { SubscriptionStatusComponent } from "./subscription-status.component"; SecretsManagerSubscribeStandaloneComponent, SubscriptionHiddenComponent, SubscriptionStatusComponent, - ChangePlanDialogComponent, OrganizationPaymentMethodComponent, ], }) diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts index efd207bbad0..7a10fc0a434 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, lastValueFrom, Observable, Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -42,6 +42,9 @@ import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.comp templateUrl: "organization-subscription-cloud.component.html", }) export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy { + static readonly QUERY_PARAM_UPGRADE: string = "upgrade"; + static readonly ROUTE_PARAM_ORGANIZATION_ID: string = "organizationId"; + sub: OrganizationSubscriptionResponse; lineItems: BillingSubscriptionItemResponse[] = []; organizationId: string; @@ -82,7 +85,19 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy ) {} async ngOnInit() { - if (this.route.snapshot.queryParamMap.get("upgrade")) { + this.organizationId = + this.route.snapshot.params[ + OrganizationSubscriptionCloudComponent.ROUTE_PARAM_ORGANIZATION_ID + ]; + await this.load(); + + this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( + FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, + ); + + if ( + this.route.snapshot.queryParams[OrganizationSubscriptionCloudComponent.QUERY_PARAM_UPGRADE] + ) { await this.changePlan(); const productTierTypeStr = this.route.snapshot.queryParamMap.get("productTierType"); if (productTierTypeStr != null) { @@ -92,20 +107,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy } } } - - this.route.params - .pipe( - concatMap(async (params) => { - this.organizationId = params.organizationId; - await this.load(); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( - FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, - ); } ngOnDestroy() { diff --git a/apps/web/src/app/billing/services/reseller-warning.service.ts b/apps/web/src/app/billing/services/reseller-warning.service.ts index bfd5be3233a..2c59ebafe05 100644 --- a/apps/web/src/app/billing/services/reseller-warning.service.ts +++ b/apps/web/src/app/billing/services/reseller-warning.service.ts @@ -33,7 +33,7 @@ export class ResellerWarningService { return { type: "warning", message: this.i18nService.t( - "resellerPastDueWarning", + "resellerPastDueWarningMsg", organization.providerName, this.formatDate(gracePeriodEnd), ), @@ -50,7 +50,7 @@ export class ResellerWarningService { return { type: "info", message: this.i18nService.t( - "resellerOpenInvoiceWarning", + "resellerOpenInvoiceWarningMgs", organization.providerName, this.formatDate(organizationBillingMetadata.invoiceCreatedDate), this.formatDate(organizationBillingMetadata.invoiceDueDate), @@ -68,7 +68,7 @@ export class ResellerWarningService { return { type: "info", message: this.i18nService.t( - "resellerRenewalWarning", + "resellerRenewalWarningMsg", organization.providerName, this.formatDate(organizationBillingMetadata.subPeriodEndDate), ), diff --git a/apps/web/src/app/billing/services/trial-flow.service.ts b/apps/web/src/app/billing/services/trial-flow.service.ts index 558851ad64c..a3a4ba6bba1 100644 --- a/apps/web/src/app/billing/services/trial-flow.service.ts +++ b/apps/web/src/app/billing/services/trial-flow.service.ts @@ -2,25 +2,37 @@ // @ts-strict-ignore import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; +import { lastValueFrom } from "rxjs"; +import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { DialogService } from "@bitwarden/components"; import { FreeTrial } from "../../core/types/free-trial"; +import { + ChangePlanDialogResultType, + openChangePlanDialog, +} from "../organizations/change-plan-dialog.component"; @Injectable({ providedIn: "root" }) export class TrialFlowService { + private resellerManagedOrgAlert: boolean; + constructor( private i18nService: I18nService, protected dialogService: DialogService, private router: Router, protected billingApiService: BillingApiServiceAbstraction, + private organizationApiService: OrganizationApiServiceAbstraction, + private configService: ConfigService, ) {} checkForOrgsWithUpcomingPaymentIssues( organization: Organization, @@ -66,16 +78,31 @@ export class TrialFlowService { org: Organization, organizationBillingMetadata: OrganizationBillingMetadataResponse, ): Promise { - if (organizationBillingMetadata.isSubscriptionUnpaid) { - const confirmed = await this.promptForPaymentNavigation(org); + if ( + organizationBillingMetadata.isSubscriptionUnpaid || + organizationBillingMetadata.isSubscriptionCanceled + ) { + const confirmed = await this.promptForPaymentNavigation( + org, + organizationBillingMetadata.isSubscriptionCanceled, + organizationBillingMetadata.isSubscriptionUnpaid, + ); if (confirmed) { await this.navigateToPaymentMethod(org?.id); } } } - private async promptForPaymentNavigation(org: Organization): Promise { - if (!org?.isOwner) { + private async promptForPaymentNavigation( + org: Organization, + isCanceled: boolean, + isUnpaid: boolean, + ): Promise { + this.resellerManagedOrgAlert = await this.configService.getFeatureFlag( + FeatureFlag.ResellerManagedOrgAlert, + ); + + if (!org?.isOwner && !org.providerId) { await this.dialogService.openSimpleDialog({ title: this.i18nService.t("suspendedOrganizationTitle", org?.name), content: { key: "suspendedUserOrgMessage" }, @@ -85,13 +112,31 @@ export class TrialFlowService { }); return false; } - return await this.dialogService.openSimpleDialog({ - title: this.i18nService.t("suspendedOrganizationTitle", org?.name), - content: { key: "suspendedOwnerOrgMessage" }, - type: "danger", - acceptButtonText: this.i18nService.t("continue"), - cancelButtonText: this.i18nService.t("close"), - }); + + if (org.providerId && this.resellerManagedOrgAlert) { + await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedManagedOrgMessage", placeholders: [org.providerName] }, + type: "danger", + acceptButtonText: this.i18nService.t("close"), + cancelButtonText: null, + }); + return false; + } + + if (org.isOwner && isUnpaid) { + return await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("suspendedOrganizationTitle", org.name), + content: { key: "suspendedOwnerOrgMessage" }, + type: "danger", + acceptButtonText: this.i18nService.t("continue"), + cancelButtonText: this.i18nService.t("close"), + }); + } + + if (org.isOwner && isCanceled && this.resellerManagedOrgAlert) { + await this.changePlan(org); + } } private async navigateToPaymentMethod(orgId: string) { @@ -99,4 +144,20 @@ export class TrialFlowService { state: { launchPaymentModalAutomatically: true }, }); } + + private async changePlan(org: Organization) { + const subscription = await this.organizationApiService.getSubscription(org.id); + const reference = openChangePlanDialog(this.dialogService, { + data: { + organizationId: org.id, + subscription: subscription, + productTierType: org.productTierType, + }, + }); + + const result = await lastValueFrom(reference.closed); + if (result === ChangePlanDialogResultType.Closed) { + return; + } + } } diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 2dd1db9fdb6..8f21dfa2c8b 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -30,7 +30,6 @@ import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/serv import { RegistrationFinishService as RegistrationFinishServiceAbstraction, LoginComponentService, - LockComponentService, SetPasswordJitService, SsoComponentService, LoginDecryptionOptionsService, @@ -92,6 +91,7 @@ import { KeyService as KeyServiceAbstraction, BiometricsService, } from "@bitwarden/key-management"; +import { LockComponentService } from "@bitwarden/key-management/angular"; import { flagEnabled } from "../../utils/flags"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -99,13 +99,13 @@ import { WebSetPasswordJitService, WebRegistrationFinishService, WebLoginComponentService, - WebLockComponentService, WebLoginDecryptionOptionsService, } from "../auth"; import { WebSsoComponentService } from "../auth/core/services/login/web-sso-component.service"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; +import { WebLockComponentService } from "../key-management/lock/services/web-lock-component.service"; import { WebProcessReloadService } from "../key-management/services/web-process-reload.service"; import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; diff --git a/apps/web/src/app/core/guards/has-premium.guard.ts b/apps/web/src/app/core/guards/has-premium.guard.ts index ab544dafb61..61853b25cb8 100644 --- a/apps/web/src/app/core/guards/has-premium.guard.ts +++ b/apps/web/src/app/core/guards/has-premium.guard.ts @@ -6,9 +6,10 @@ import { CanActivateFn, UrlTree, } from "@angular/router"; -import { Observable } from "rxjs"; -import { tap } from "rxjs/operators"; +import { Observable, of } from "rxjs"; +import { switchMap, tap } from "rxjs/operators"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; @@ -24,8 +25,14 @@ export function hasPremiumGuard(): CanActivateFn { const router = inject(Router); const messagingService = inject(MessagingService); const billingAccountProfileStateService = inject(BillingAccountProfileStateService); + const accountService = inject(AccountService); - return billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( + return accountService.activeAccount$.pipe( + switchMap((account) => + account + ? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), tap((userHasPremium: boolean) => { if (!userHasPremium) { messagingService.send("premiumRequired"); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts similarity index 95% rename from apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts rename to apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts index 5eb26a8c76c..3c941fe24c7 100644 --- a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; import { WebLockComponentService } from "./web-lock-component.service"; @@ -86,7 +87,7 @@ describe("WebLockComponentService", () => { }, biometrics: { enabled: false, - disableReason: null, + biometricsStatus: BiometricsStatus.PlatformUnsupported, }, }); }); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.ts b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts similarity index 90% rename from apps/web/src/app/auth/core/services/web-lock-component.service.ts rename to apps/web/src/app/key-management/lock/services/web-lock-component.service.ts index e24f299e23b..02910966d6e 100644 --- a/apps/web/src/app/auth/core/services/web-lock-component.service.ts +++ b/apps/web/src/app/key-management/lock/services/web-lock-component.service.ts @@ -1,12 +1,13 @@ import { inject } from "@angular/core"; import { map, Observable } from "rxjs"; -import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular"; import { UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsStatus } from "@bitwarden/key-management"; +import { LockComponentService, UnlockOptions } from "@bitwarden/key-management/angular"; export class WebLockComponentService implements LockComponentService { private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); @@ -45,7 +46,7 @@ export class WebLockComponentService implements LockComponentService { }, biometrics: { enabled: false, - disableReason: null, + biometricsStatus: BiometricsStatus.PlatformUnsupported, }, }; return unlockOpts; diff --git a/apps/web/src/app/key-management/web-biometric.service.ts b/apps/web/src/app/key-management/web-biometric.service.ts index 4681eb6fa49..0c58c0da759 100644 --- a/apps/web/src/app/key-management/web-biometric.service.ts +++ b/apps/web/src/app/key-management/web-biometric.service.ts @@ -1,27 +1,27 @@ -import { BiometricsService } from "@bitwarden/key-management"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { BiometricsService, BiometricsStatus } from "@bitwarden/key-management"; export class WebBiometricsService extends BiometricsService { - async supportsBiometric(): Promise { + async authenticateWithBiometrics(): Promise { return false; } - async isBiometricUnlockAvailable(): Promise { - return false; + async getBiometricsStatus(): Promise { + return BiometricsStatus.PlatformUnsupported; } - async authenticateBiometric(): Promise { - throw new Error("Method not implemented."); + async unlockWithBiometricsForUser(userId: UserId): Promise { + return null; } - async biometricsNeedsSetup(): Promise { - throw new Error("Method not implemented."); + async getBiometricsStatusForUser(userId: UserId): Promise { + return BiometricsStatus.PlatformUnsupported; } - async biometricsSupportsAutoSetup(): Promise { - throw new Error("Method not implemented."); + async getShouldAutopromptNow(): Promise { + return false; } - async biometricsSetup(): Promise { - throw new Error("Method not implemented."); - } + async setShouldAutopromptNow(value: boolean): Promise {} } diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index 18277abebef..f0ac3ef9b48 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -3,12 +3,11 @@ import { CommonModule } from "@angular/common"; import { Component, OnInit } from "@angular/core"; import { RouterModule } from "@angular/router"; -import { Observable, concatMap, combineLatest } from "rxjs"; +import { Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/platform/sync"; import { IconModule } from "@bitwarden/components"; @@ -38,35 +37,19 @@ export class UserLayoutComponent implements OnInit { protected showSubscription$: Observable; constructor( - private platformUtilsService: PlatformUtilsService, - private apiService: ApiService, private syncService: SyncService, private billingAccountProfileStateService: BillingAccountProfileStateService, - ) {} + private accountService: AccountService, + ) { + this.showSubscription$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.canViewSubscription$(account.id), + ), + ); + } async ngOnInit() { document.body.classList.remove("layout_frontend"); - await this.syncService.fullSync(false); - - // We want to hide the subscription menu for organizations that provide premium. - // Except if the user has premium personally or has a billing history. - this.showSubscription$ = combineLatest([ - this.billingAccountProfileStateService.hasPremiumPersonally$, - this.billingAccountProfileStateService.hasPremiumFromAnyOrganization$, - ]).pipe( - concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { - const isCloud = !this.platformUtilsService.isSelfHost(); - - let billing = null; - if (isCloud) { - // TODO: We should remove the need to call this! - billing = await this.apiService.getUserBillingHistory(); - } - - const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; - return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; - }), - ); } } diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index ad536110b74..fadcc28f832 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -25,7 +25,6 @@ import { RegistrationLinkExpiredComponent, LoginComponent, LoginSecondaryContentComponent, - LockComponent, LockIcon, TwoFactorTimeoutIcon, UserLockIcon, @@ -40,6 +39,7 @@ import { LoginDecryptionOptionsComponent, } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { LockComponent } from "@bitwarden/key-management/angular"; import { NewDeviceVerificationNoticePageOneComponent, NewDeviceVerificationNoticePageTwoComponent, diff --git a/apps/web/src/app/platform/web-sdk-client-factory.ts b/apps/web/src/app/platform/web-sdk-client-factory.ts index 2ebb2bcc10f..0dd43ecbb92 100644 --- a/apps/web/src/app/platform/web-sdk-client-factory.ts +++ b/apps/web/src/app/platform/web-sdk-client-factory.ts @@ -27,6 +27,8 @@ const supported = (() => { return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; } } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // ignore } diff --git a/apps/web/src/app/tools/reports/pages/reports-home.component.ts b/apps/web/src/app/tools/reports/pages/reports-home.component.ts index 961c24bb017..604d66f6858 100644 --- a/apps/web/src/app/tools/reports/pages/reports-home.component.ts +++ b/apps/web/src/app/tools/reports/pages/reports-home.component.ts @@ -3,6 +3,7 @@ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { reports, ReportType } from "../reports"; @@ -15,11 +16,15 @@ import { ReportEntry, ReportVariant } from "../shared"; export class ReportsHomeComponent implements OnInit { reports: ReportEntry[]; - constructor(private billingAccountProfileStateService: BillingAccountProfileStateService) {} + constructor( + private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, + ) {} async ngOnInit(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); const userHasPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); const reportRequiresPremium = userHasPremium ? ReportVariant.Enabled diff --git a/apps/web/src/app/tools/send/send-access-file.component.ts b/apps/web/src/app/tools/send/send-access-file.component.ts index 0b2a971bbe8..b55e955f355 100644 --- a/apps/web/src/app/tools/send/send-access-file.component.ts +++ b/apps/web/src/app/tools/send/send-access-file.component.ts @@ -70,6 +70,8 @@ export class SendAccessFileComponent { blobData: decBuf, downloadMethod: "save", }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/apps/web/src/app/vault/components/premium-badge.stories.ts b/apps/web/src/app/vault/components/premium-badge.stories.ts index 17622dbbd5f..331f72fd0ac 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -2,6 +2,7 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { MessageSender } from "@bitwarden/common/platform/messaging"; @@ -22,6 +23,14 @@ export default { moduleMetadata({ imports: [JslibModule, BadgeModule], providers: [ + { + provide: AccountService, + useValue: { + activeAccount$: of({ + id: "123", + }), + }, + }, { provide: I18nService, useFactory: () => { @@ -39,7 +48,7 @@ export default { { provide: BillingAccountProfileStateService, useValue: { - hasPremiumFromAnySource$: of(false), + hasPremiumFromAnySource$: () => of(false), }, }, ], diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index a9ff49c5791..59638aad653 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -4,7 +4,7 @@ import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component, ElementRef, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { Router } from "@angular/router"; -import { firstValueFrom, Observable, Subject } from "rxjs"; +import { firstValueFrom, Observable, Subject, switchMap } from "rxjs"; import { map } from "rxjs/operators"; import { CollectionView } from "@bitwarden/admin-console/common"; @@ -40,6 +40,7 @@ import { CipherFormGenerationService, CipherFormModule, CipherViewComponent, + DecryptionFailureDialogComponent, } from "@bitwarden/vault"; import { SharedModule } from "../../../shared/shared.module"; @@ -114,6 +115,7 @@ export enum VaultItemDialogResult { CipherAttachmentsComponent, AsyncActionsModule, ItemModule, + DecryptionFailureDialogComponent, ], providers: [ { provide: PremiumUpgradePromptService, useClass: WebVaultPremiumUpgradePromptService }, @@ -183,7 +185,11 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { * Flag to indicate if the user has access to attachments via a premium subscription. * @protected */ - protected canAccessAttachments$ = this.billingAccountProfileStateService.hasPremiumFromAnySource$; + protected canAccessAttachments$ = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); protected get loadingForm() { return this.loadForm && !this.formReady; @@ -248,6 +254,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.cipher = await this.getDecryptedCipherView(this.formConfig); if (this.cipher) { + if (this.cipher.decryptionFailure) { + this.dialogService.open(DecryptionFailureDialogComponent, { + data: { cipherIds: [this.cipher.id] }, + }); + this.dialogRef.close(); + return; + } + this.collections = this.formConfig.collections.filter((c) => this.cipher.collectionIds?.includes(c.id), ); diff --git a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html index 5c4de576ead..7e59853851c 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-cipher-row.component.html @@ -4,7 +4,7 @@ type="checkbox" bitCheckbox appStopProp - [disabled]="disabled" + [disabled]="disabled || cipher.decryptionFailure" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" [attr.aria-label]="'vaultItemSelect' | i18n" @@ -20,7 +20,7 @@ class="tw-overflow-hidden tw-text-ellipsis tw-text-start tw-leading-snug" [disabled]="disabled" [routerLink]="[]" - [queryParams]="{ itemId: cipher.id, action: extensionRefreshEnabled ? 'view' : null }" + [queryParams]="{ itemId: cipher.id, action: clickAction }" queryParamsHandling="merge" [replaceUrl]="extensionRefreshEnabled" title="{{ 'editItemWithName' | i18n: cipher.name }}" @@ -76,6 +76,25 @@ + + + + + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..d6a757fe897 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/app-at-risk-members-dialog.component.ts @@ -0,0 +1,35 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { MemberDetailsFlat } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; + +type AppAtRiskMembersDialogParams = { + members: MemberDetailsFlat[]; + applicationName: string; +}; + +export const openAppAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AppAtRiskMembersDialogParams, +) => + dialogService.open(AppAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./app-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, JslibModule, DialogModule], +}) +export class AppAtRiskMembersDialogComponent { + protected members: MemberDetailsFlat[]; + protected applicationName: string; + + constructor(@Inject(DIALOG_DATA) private params: AppAtRiskMembersDialogParams) { + this.members = params.members; + this.applicationName = params.applicationName; + } +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html new file mode 100644 index 00000000000..298011b2157 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.html @@ -0,0 +1,25 @@ + + + {{ "atRiskApplicationsWithCount" | i18n: atRiskApps.length }} + + +

+ {{ "atRiskApplicationsDescription" | i18n }} +
+
{{ "application" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ app.applicationName }}
+
{{ app.atRiskPasswordCount }}
+
+
+
+
+ + + + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts new file mode 100644 index 00000000000..0ae00f60874 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-apps-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskApplicationDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskApplicationDetail[], +) => + dialogService.open(OrgAtRiskAppsDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-apps-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskAppsDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskApps: AtRiskApplicationDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html new file mode 100644 index 00000000000..41ac8af7886 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.html @@ -0,0 +1,27 @@ + + + {{ "atRiskMembersWithCount" | i18n: atRiskMembers.length }} + + +
+ {{ + "atRiskMembersDescription" | i18n + }} +
+
{{ "email" | i18n }}
+
{{ "atRiskPasswords" | i18n }}
+
+ +
+
{{ member.email }}
+
{{ member.atRiskPasswordCount }}
+
+
+
+
+ + + +
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts new file mode 100644 index 00000000000..72518843d94 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/org-at-risk-members-dialog.component.ts @@ -0,0 +1,24 @@ +import { DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, Inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AtRiskMemberDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; +import { ButtonModule, DialogModule, DialogService, TypographyModule } from "@bitwarden/components"; + +export const openOrgAtRiskMembersDialog = ( + dialogService: DialogService, + dialogConfig: AtRiskMemberDetail[], +) => + dialogService.open(OrgAtRiskMembersDialogComponent, { + data: dialogConfig, + }); + +@Component({ + standalone: true, + templateUrl: "./org-at-risk-members-dialog.component.html", + imports: [ButtonModule, CommonModule, DialogModule, JslibModule, TypographyModule], +}) +export class OrgAtRiskMembersDialogComponent { + constructor(@Inject(DIALOG_DATA) protected atRiskMembers: AtRiskMemberDetail[]) {} +} diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html index e0618c525a7..7fe320ede6a 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html @@ -3,7 +3,6 @@

{{ "riskInsights" | i18n }}

{{ "reviewAtRiskPasswords" | i18n }} -  {{ "learnMore" | i18n }}
{{ "riskInsights" | i18n }}

- + - + - + diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts index 1a90e18f0df..75601994c70 100644 --- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.ts @@ -10,6 +10,7 @@ import { RiskInsightsDataService } from "@bitwarden/bit-common/tools/reports/ris import { ApplicationHealthReportDetail } from "@bitwarden/bit-common/tools/reports/risk-insights/models/password-health"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { AsyncActionsModule, ButtonModule, TabsModule } from "@bitwarden/components"; import { HeaderModule } from "@bitwarden/web-vault/app/layouts/header/header.module"; @@ -50,6 +51,7 @@ export class RiskInsightsComponent implements OnInit { dataLastUpdated: Date = new Date(); isCriticalAppsFeatureEnabled: boolean = false; + showDebugTabs: boolean = false; appsCount: number = 0; criticalAppsCount: number = 0; @@ -78,6 +80,8 @@ export class RiskInsightsComponent implements OnInit { FeatureFlag.CriticalApps, ); + this.showDebugTabs = devFlagEnabled("showRiskInsightsDebug"); + this.route.paramMap .pipe( takeUntilDestroyed(this.destroyRef), diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts index 6aaaf1a4066..321aae165c5 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/member-access-report.component.ts @@ -103,7 +103,7 @@ export class MemberAccessReportComponent implements OnInit { usesKeyConnector: user?.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: MemberDialogTab.Role, - numConfirmedMembers: this.dataSource.data.length, + numSeatsUsed: this.dataSource.data.length, }, }); diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts index 9919da8d08e..cf2f3b6417b 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.abstraction.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessExportItem } from "../view/member-access-export.view"; import { MemberAccessReportView } from "../view/member-access-report.view"; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts index 6aab54f77d5..7d6beca48ec 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { OrganizationId } from "@bitwarden/common/src/types/guid"; +import { OrganizationId } from "@bitwarden/common/types/guid"; import { MemberAccessReportApiService } from "./member-access-report-api.service"; import { memberAccessReportsMock } from "./member-access-report.mock"; diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 09de92d355d..c4304ec2bd9 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -24,6 +24,7 @@ "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/key-management": ["../../libs/key-management/src"], + "@bitwarden/key-management/angular": ["../../libs/key-management/src/angular"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/send-ui": ["../../libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["../../libs/tools/card/src"], diff --git a/clients.code-workspace b/clients.code-workspace index 1b956c25cee..f7d86d2a242 100644 --- a/clients.code-workspace +++ b/clients.code-workspace @@ -73,6 +73,15 @@ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "Angular.ng-template", + "nick-rudenko.back-n-forth", + "streetsidesoftware.code-spell-checker", + "MS-vsliveshare.vsliveshare", + "mhutchie.git-graph", + "donjayamanne.githistory", + "eamodio.gitlens", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "vadimcn.vscode-lldb", ], }, } diff --git a/libs/admin-console/jest.config.js b/libs/admin-console/jest.config.js index f2a8e6458af..5131753964c 100644 --- a/libs/admin-console/jest.config.js +++ b/libs/admin-console/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/admin-console/tsconfig.json b/libs/admin-console/tsconfig.json index 6004a56fb55..3d22cb2ec51 100644 --- a/libs/admin-console/tsconfig.json +++ b/libs/admin-console/tsconfig.json @@ -1,5 +1,13 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/key-management": ["../key-management/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/angular/jest.config.js b/libs/angular/jest.config.js index c8e748575c0..5e73614eb8e 100644 --- a/libs/angular/jest.config.js +++ b/libs/angular/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/angular/src/auth/components/login-v1.component.ts b/libs/angular/src/auth/components/login-v1.component.ts index 0775114bd8c..ffe1dda3aed 100644 --- a/libs/angular/src/auth/components/login-v1.component.ts +++ b/libs/angular/src/auth/components/login-v1.component.ts @@ -397,6 +397,8 @@ export class LoginComponentV1 extends CaptchaProtectedComponent implements OnIni email, deviceIdentifier, ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.showLoginWithDevice = false; } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index 18bfe546600..e2b41ad086d 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -157,7 +157,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.toastService.showToast({ variant: "error", title: this.i18nService.t("errorOccurred"), - message: error, + message: this.i18nService.t("webauthnCancelOrTimeout"), }); }, (info: string) => { diff --git a/libs/angular/src/directives/copy-click.directive.ts b/libs/angular/src/directives/copy-click.directive.ts index cb346ebf8d3..ece867c09fd 100644 --- a/libs/angular/src/directives/copy-click.directive.ts +++ b/libs/angular/src/directives/copy-click.directive.ts @@ -4,8 +4,7 @@ import { Directive, HostListener, Input } from "@angular/core"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { ToastService } from "@bitwarden/components"; -import { ToastVariant } from "@bitwarden/components/src/toast/toast.component"; +import { ToastService, ToastVariant } from "@bitwarden/components"; @Directive({ selector: "[appCopyClick]", diff --git a/libs/angular/src/directives/not-premium.directive.ts b/libs/angular/src/directives/not-premium.directive.ts index 3aee9b192d2..5a1c636c009 100644 --- a/libs/angular/src/directives/not-premium.directive.ts +++ b/libs/angular/src/directives/not-premium.directive.ts @@ -1,6 +1,7 @@ import { Directive, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; /** @@ -14,11 +15,19 @@ export class NotPremiumDirective implements OnInit { private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async ngOnInit(): Promise { + const account = await firstValueFrom(this.accountService.activeAccount$); + + if (!account) { + this.viewContainer.createEmbeddedView(this.templateRef); + return; + } + const premium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), ); if (premium) { diff --git a/libs/angular/src/directives/premium.directive.ts b/libs/angular/src/directives/premium.directive.ts index d475669a1ab..2188205ba65 100644 --- a/libs/angular/src/directives/premium.directive.ts +++ b/libs/angular/src/directives/premium.directive.ts @@ -1,6 +1,7 @@ import { Directive, OnDestroy, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { Subject, takeUntil } from "rxjs"; +import { of, Subject, switchMap, takeUntil } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; /** @@ -16,16 +17,24 @@ export class PremiumDirective implements OnInit, OnDestroy { private templateRef: TemplateRef, private viewContainer: ViewContainerRef, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} async ngOnInit(): Promise { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.directiveIsDestroyed$)) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + account + ? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + takeUntil(this.directiveIsDestroyed$), + ) .subscribe((premium: boolean) => { if (premium) { - this.viewContainer.clear(); - } else { this.viewContainer.createEmbeddedView(this.templateRef); + } else { + this.viewContainer.clear(); } }); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 583ba82fc98..803808612cf 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -20,6 +20,7 @@ import { DefaultLoginComponentService, LoginDecryptionOptionsService, DefaultLoginDecryptionOptionsService, + DefaultLoginApprovalComponentService, } from "@bitwarden/auth/angular"; import { AuthRequestServiceAbstraction, @@ -39,6 +40,7 @@ import { DefaultAuthRequestApiService, DefaultLoginSuccessHandlerService, LoginSuccessHandlerService, + LoginApprovalComponentServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -279,12 +281,13 @@ import { ImportServiceAbstraction, } from "@bitwarden/importer/core"; import { - KeyService as KeyServiceAbstraction, - DefaultKeyService as KeyService, + KeyService, + DefaultKeyService, BiometricStateService, DefaultBiometricStateService, - KdfConfigService, + BiometricsService, DefaultKdfConfigService, + KdfConfigService, UserAsymmetricKeysRegenerationService, DefaultUserAsymmetricKeysRegenerationService, UserAsymmetricKeysRegenerationApiService, @@ -300,6 +303,8 @@ import { IndividualVaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../vault/src/services/new-device-verification-notice.service"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; import { ViewCacheService } from "../platform/abstractions/view-cache.service"; @@ -416,7 +421,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, MessagingServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, StateServiceAbstraction, TokenServiceAbstraction, @@ -428,7 +433,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, TokenServiceAbstraction, AppIdServiceAbstraction, @@ -471,7 +476,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CipherServiceAbstraction, useFactory: ( - keyService: KeyServiceAbstraction, + keyService: KeyService, domainSettingsService: DomainSettingsService, apiService: ApiServiceAbstraction, i18nService: I18nServiceAbstraction, @@ -501,7 +506,7 @@ const safeProviders: SafeProvider[] = [ accountService, ), deps: [ - KeyServiceAbstraction, + KeyService, DomainSettingsService, ApiServiceAbstraction, I18nServiceAbstraction, @@ -520,7 +525,7 @@ const safeProviders: SafeProvider[] = [ provide: InternalFolderService, useClass: FolderService, deps: [ - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, CipherServiceAbstraction, @@ -565,7 +570,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: CollectionService, useClass: DefaultCollectionService, - deps: [KeyServiceAbstraction, EncryptService, I18nServiceAbstraction, StateProvider], + deps: [KeyService, EncryptService, I18nServiceAbstraction, StateProvider], }), safeProvider({ provide: ENV_ADDITIONAL_REGIONS, @@ -610,8 +615,8 @@ const safeProviders: SafeProvider[] = [ deps: [CryptoFunctionServiceAbstraction], }), safeProvider({ - provide: KeyServiceAbstraction, - useClass: KeyService, + provide: KeyService, + useClass: DefaultKeyService, deps: [ PinServiceAbstraction, InternalMasterPasswordServiceAbstraction, @@ -636,7 +641,7 @@ const safeProviders: SafeProvider[] = [ useFactory: legacyPasswordGenerationServiceFactory, deps: [ EncryptService, - KeyServiceAbstraction, + KeyService, PolicyServiceAbstraction, AccountServiceAbstraction, StateProvider, @@ -645,7 +650,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: GeneratorHistoryService, useClass: LocalGeneratorHistoryService, - deps: [EncryptService, KeyServiceAbstraction, StateProvider], + deps: [EncryptService, KeyService, StateProvider], }), safeProvider({ provide: UsernameGenerationServiceAbstraction, @@ -653,7 +658,7 @@ const safeProviders: SafeProvider[] = [ deps: [ ApiServiceAbstraction, I18nServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, PolicyServiceAbstraction, AccountServiceAbstraction, @@ -693,7 +698,7 @@ const safeProviders: SafeProvider[] = [ provide: InternalSendService, useClass: SendService, deps: [ - KeyServiceAbstraction, + KeyService, I18nServiceAbstraction, KeyGenerationServiceAbstraction, SendStateProviderAbstraction, @@ -720,7 +725,7 @@ const safeProviders: SafeProvider[] = [ DomainSettingsService, InternalFolderService, CipherServiceAbstraction, - KeyServiceAbstraction, + KeyService, CollectionService, MessagingServiceAbstraction, InternalPolicyService, @@ -753,7 +758,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, PinServiceAbstraction, UserDecryptionOptionsServiceAbstraction, - KeyServiceAbstraction, + KeyService, TokenServiceAbstraction, PolicyServiceAbstraction, BiometricStateService, @@ -780,6 +785,7 @@ const safeProviders: SafeProvider[] = [ StateEventRunnerService, TaskSchedulerService, LogService, + BiometricsService, LOCKED_CALLBACK, LOGOUT_CALLBACK, ], @@ -826,7 +832,7 @@ const safeProviders: SafeProvider[] = [ ImportApiServiceAbstraction, I18nServiceAbstraction, CollectionService, - KeyServiceAbstraction, + KeyService, EncryptService, PinServiceAbstraction, AccountServiceAbstraction, @@ -839,7 +845,7 @@ const safeProviders: SafeProvider[] = [ FolderServiceAbstraction, CipherServiceAbstraction, PinServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, CryptoFunctionServiceAbstraction, KdfConfigService, @@ -853,7 +859,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, ApiServiceAbstraction, PinServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, CryptoFunctionServiceAbstraction, CollectionService, @@ -960,7 +966,7 @@ const safeProviders: SafeProvider[] = [ deps: [ AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, ApiServiceAbstraction, TokenServiceAbstraction, LogService, @@ -974,17 +980,15 @@ const safeProviders: SafeProvider[] = [ provide: UserVerificationServiceAbstraction, useClass: UserVerificationService, deps: [ - KeyServiceAbstraction, + KeyService, AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, UserDecryptionOptionsServiceAbstraction, PinServiceAbstraction, - LogService, - VaultTimeoutSettingsServiceAbstraction, - PlatformUtilsServiceAbstraction, KdfConfigService, + BiometricsService, ], }), safeProvider({ @@ -1007,7 +1011,7 @@ const safeProviders: SafeProvider[] = [ deps: [ OrganizationApiServiceAbstraction, AccountServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, OrganizationUserApiService, I18nServiceAbstraction, @@ -1109,7 +1113,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DevicesServiceAbstraction, useClass: DevicesServiceImplementation, - deps: [DevicesApiServiceAbstraction], + deps: [DevicesApiServiceAbstraction, AppIdServiceAbstraction], }), safeProvider({ provide: DeviceTrustServiceAbstraction, @@ -1117,7 +1121,7 @@ const safeProviders: SafeProvider[] = [ deps: [ KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, AppIdServiceAbstraction, DevicesApiServiceAbstraction, @@ -1137,7 +1141,7 @@ const safeProviders: SafeProvider[] = [ AppIdServiceAbstraction, AccountServiceAbstraction, InternalMasterPasswordServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, ApiServiceAbstraction, StateProvider, @@ -1231,7 +1235,7 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, BillingApiServiceAbstraction, ConfigService, - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, @@ -1281,7 +1285,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: BillingAccountProfileStateService, useClass: DefaultBillingAccountProfileStateService, - deps: [StateProvider], + deps: [StateProvider, PlatformUtilsServiceAbstraction, ApiServiceAbstraction], }), safeProvider({ provide: OrganizationManagementPreferencesService, @@ -1291,7 +1295,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: UserAutoUnlockKeyService, useClass: UserAutoUnlockKeyService, - deps: [KeyServiceAbstraction], + deps: [KeyService], }), safeProvider({ provide: ErrorHandler, @@ -1335,7 +1339,7 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultSetPasswordJitService, deps: [ ApiServiceAbstraction, - KeyServiceAbstraction, + KeyService, EncryptService, I18nServiceAbstraction, KdfConfigService, @@ -1363,7 +1367,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: RegistrationFinishServiceAbstraction, useClass: DefaultRegistrationFinishService, - deps: [KeyServiceAbstraction, AccountApiServiceAbstraction], + deps: [KeyService, AccountApiServiceAbstraction], }), safeProvider({ provide: ViewCacheService, @@ -1390,7 +1394,7 @@ const safeProviders: SafeProvider[] = [ PlatformUtilsServiceAbstraction, AccountServiceAbstraction, KdfConfigService, - KeyServiceAbstraction, + KeyService, ], }), safeProvider({ @@ -1403,6 +1407,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAuthRequestApiService, deps: [ApiServiceAbstraction, LogService], }), + safeProvider({ + provide: LoginApprovalComponentServiceAbstraction, + useClass: DefaultLoginApprovalComponentService, + deps: [], + }), safeProvider({ provide: LoginDecryptionOptionsService, useClass: DefaultLoginDecryptionOptionsService, @@ -1418,7 +1427,7 @@ const safeProviders: SafeProvider[] = [ provide: UserAsymmetricKeysRegenerationService, useClass: DefaultUserAsymmetricKeysRegenerationService, deps: [ - KeyServiceAbstraction, + KeyService, CipherServiceAbstraction, UserAsymmetricKeysRegenerationApiService, LogService, diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index 228abec98a9..aeee1fa104c 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -3,7 +3,15 @@ import { DatePipe } from "@angular/common"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } from "rxjs"; +import { + Subject, + firstValueFrom, + takeUntil, + map, + BehaviorSubject, + concatMap, + switchMap, +} from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -197,8 +205,13 @@ export class AddEditComponent implements OnInit, OnDestroy { const env = await firstValueFrom(this.environmentService.environment$); this.sendLinkBaseUrl = env.getSendUrl(); - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.destroy$)) + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntil(this.destroy$), + ) .subscribe((hasPremiumFromAnySource) => { this.canAccessPremium = hasPremiumFromAnySource; }); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 4bd3e9710fb..bf2e68b71cd 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -16,7 +16,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin- import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; -import { ClientType, EventType } from "@bitwarden/common/enums"; +import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -24,6 +24,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -40,7 +41,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note.view"; import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; +import { generate_ssh_key } from "@bitwarden/sdk-internal"; import { PasswordRepromptService } from "@bitwarden/vault"; @Directive() @@ -126,12 +128,14 @@ export class AddEditComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected logService: LogService, protected passwordRepromptService: PasswordRepromptService, - private organizationService: OrganizationService, + protected organizationService: OrganizationService, protected dialogService: DialogService, protected win: Window, protected datePipe: DatePipe, protected configService: ConfigService, protected cipherAuthorizationService: CipherAuthorizationService, + protected toastService: ToastService, + private sdkService: SdkService, ) { this.typeOptions = [ { name: i18nService.t("typeLogin"), value: CipherType.Login }, @@ -208,7 +212,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.canUseReprompt = await this.passwordRepromptService.enabled(); const sshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem); - if (this.platformUtilsService.getClientType() == ClientType.Desktop && sshKeysEnabled) { + if (sshKeysEnabled) { this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey }); } } @@ -339,6 +343,17 @@ export class AddEditComponent implements OnInit, OnDestroy { [this.collectionId as CollectionId], this.isAdminConsoleAction, ); + + if (!this.editMode || this.cloneMode) { + // Creating an ssh key directly while filtering to the ssh key category + // must force a key to be set. SSH keys must never be created with an empty private key field + if ( + this.cipher.type === CipherType.SshKey && + (this.cipher.sshKey.privateKey == null || this.cipher.sshKey.privateKey === "") + ) { + await this.generateSshKey(false); + } + } } async submit(): Promise { @@ -786,4 +801,26 @@ export class AddEditComponent implements OnInit, OnDestroy { return true; } + + private async generateSshKey(showNotification: boolean = true) { + await firstValueFrom(this.sdkService.client$); + const sshKey = generate_ssh_key("Ed25519"); + this.cipher.sshKey.privateKey = sshKey.private_key; + this.cipher.sshKey.publicKey = sshKey.public_key; + this.cipher.sshKey.keyFingerprint = sshKey.key_fingerprint; + + if (showNotification) { + this.toastService.showToast({ + variant: "success", + title: "", + message: this.i18nService.t("sshKeyGenerated"), + }); + } + } + + async typeChange() { + if (this.cipher.type === CipherType.SshKey) { + await this.generateSshKey(); + } + } } diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 521d38a1f47..1a4c428aae2 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -192,6 +192,8 @@ export class AttachmentsComponent implements OnInit { title: null, message: this.i18nService.t("fileSavedToDevice"), }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } @@ -209,7 +211,7 @@ export class AttachmentsComponent implements OnInit { ); const canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.canAccessAttachments = canAccessPremium || this.cipher.organizationId != null; @@ -285,6 +287,8 @@ export class AttachmentsComponent implements OnInit { this.i18nService.t("attachmentSaved"), ); this.onReuploadedAttachment.emit(); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/libs/angular/src/vault/components/icon.component.html b/libs/angular/src/vault/components/icon.component.html index 976c6ea421d..f16545617c9 100644 --- a/libs/angular/src/vault/components/icon.component.html +++ b/libs/angular/src/vault/components/icon.component.html @@ -4,13 +4,13 @@ [src]="data.image" [appFallbackSrc]="data.fallbackImage" *ngIf="data.imageEnabled && data.image" - class="tw-max-h-6 tw-max-w-6 tw-rounded-md" + class="tw-h-6 tw-w-6 tw-rounded-md" alt="" decoding="async" loading="lazy" /> diff --git a/libs/angular/src/vault/components/premium.component.ts b/libs/angular/src/vault/components/premium.component.ts index 2ad25f2e45a..8b1f215ef42 100644 --- a/libs/angular/src/vault/components/premium.component.ts +++ b/libs/angular/src/vault/components/premium.component.ts @@ -1,9 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { OnInit, Directive } from "@angular/core"; -import { firstValueFrom, Observable } from "rxjs"; +import { firstValueFrom, Observable, switchMap } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -30,8 +31,13 @@ export class PremiumComponent implements OnInit { protected dialogService: DialogService, private environmentService: EnvironmentService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { - this.isPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.isPremium$ = accountService.activeAccount$.pipe( + switchMap((account) => + billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); } async ngOnInit() { diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index 052ab95f3bc..4ef00e90063 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; -import { BehaviorSubject, Subject, from, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, firstValueFrom, from, Subject, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -118,6 +118,12 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected async doSearch(indexedCiphers?: CipherView[]) { indexedCiphers = indexedCiphers ?? (await this.cipherService.getAllDecrypted()); + + const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); + if (failedCiphers != null && failedCiphers.length > 0) { + indexedCiphers = [...failedCiphers, ...indexedCiphers]; + } + this.ciphers = await this.searchService.searchCiphers( this.searchText, [this.filter, this.deletedFilter], diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 6bea4cd6150..ef9aff736ed 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit { showPrivateKey: boolean; canAccessPremium: boolean; showPremiumRequiredTotp: boolean; + showUpgradeRequiredTotp: boolean; totpCode: string; totpCodeFormatted: string; totpDash: number; @@ -148,25 +149,28 @@ export class ViewComponent implements OnDestroy, OnInit { await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); this.canAccessPremium = await firstValueFrom( - this.billingAccountProfileStateService.hasPremiumFromAnySource$, + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), ); this.showPremiumRequiredTotp = - this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; + this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId; this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [ this.collectionId as CollectionId, ]); + this.showUpgradeRequiredTotp = + this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp; + if (this.cipher.folderId) { this.folder = await ( await firstValueFrom(this.folderService.folderViews$(activeUserId)) ).find((f) => f.id == this.cipher.folderId); } - if ( - this.cipher.type === CipherType.Login && - this.cipher.login.totp && - (cipher.organizationUseTotp || this.canAccessPremium) - ) { + const canGenerateTotp = this.cipher.organizationId + ? this.cipher.organizationUseTotp + : this.canAccessPremium; + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) { await this.totpUpdateCode(); const interval = this.totpService.getTimeInterval(this.cipher.login.totp); await this.totpTick(interval); @@ -462,6 +466,8 @@ export class ViewComponent implements OnDestroy, OnInit { fileName: attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred")); } diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts index e278113a653..ba19cf808ee 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts index 20550e0e8cf..8b406877a12 100644 --- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts +++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts @@ -9,6 +9,8 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { NewDeviceVerificationNoticeService } from "../../../../vault/src/services/new-device-verification-notice.service"; import { VaultProfileService } from "../services/vault-profile.service"; diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index dd0b49f356a..260780e1964 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -22,6 +22,8 @@ import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { DeprecatedVaultFilterService as DeprecatedVaultFilterServiceAbstraction } from "../../abstractions/deprecated-vault-filter.service"; import { DynamicTreeNode } from "../models/dynamic-tree-node.model"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { COLLAPSED_GROUPINGS } from "./../../../../../common/src/vault/services/key-state/collapsed-groupings.state"; const NestingDelimiter = "/"; diff --git a/libs/angular/tsconfig.json b/libs/angular/tsconfig.json index 6004a56fb55..6c510f81492 100644 --- a/libs/angular/tsconfig.json +++ b/libs/angular/tsconfig.json @@ -1,5 +1,25 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/importer/core": ["../importer/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/auth/jest.config.js b/libs/auth/jest.config.js index 8bc834c7dab..121d423be17 100644 --- a/libs/auth/jest.config.js +++ b/libs/auth/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts index b07504b7c8d..9f504c75d29 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout-wrapper.stories.ts @@ -18,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { ButtonModule } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { LockIcon } from "../icons"; import { RegistrationCheckEmailIcon } from "../icons/registration-check-email.icon"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.ts b/libs/auth/src/angular/anon-layout/anon-layout.component.ts index 91229f38ab2..05ddb9614f1 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -9,8 +9,14 @@ import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IconModule, Icon } from "../../../../components/src/icon"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { TypographyModule } from "../../../../components/src/typography"; import { BitwardenLogo, BitwardenShield } from "../icons"; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts index 27eb27c53b9..c7e15d9dcfa 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -7,7 +7,11 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ButtonModule } from "../../../../components/src/button"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nMockService } from "../../../../components/src/utils/i18n-mock.service"; import { LockIcon } from "../icons"; diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index 817687ef2bc..66111f3e5af 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -57,10 +57,6 @@ export * from "./user-verification/user-verification-dialog.component"; export * from "./user-verification/user-verification-dialog.types"; export * from "./user-verification/user-verification-form-input.component"; -// lock -export * from "./lock/lock.component"; -export * from "./lock/lock-component.service"; - // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/input-password/input-password.component.ts b/libs/auth/src/angular/input-password/input-password.component.ts index 94baecb9ef2..c613cf5f533 100644 --- a/libs/auth/src/angular/input-password/input-password.component.ts +++ b/libs/auth/src/angular/input-password/input-password.component.ts @@ -26,7 +26,11 @@ import { } from "@bitwarden/components"; import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { InputsFieldMatch } from "../../../../angular/src/auth/validators/inputs-field-match.validator"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SharedModule } from "../../../../components/src/shared"; import { PasswordCalloutComponent } from "../password-callout/password-callout.component"; diff --git a/libs/auth/src/angular/input-password/input-password.stories.ts b/libs/auth/src/angular/input-password/input-password.stories.ts index 99c0aba81b8..41577328f87 100644 --- a/libs/auth/src/angular/input-password/input-password.stories.ts +++ b/libs/auth/src/angular/input-password/input-password.stories.ts @@ -14,7 +14,8 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../apps/web/src/app/core/tests"; import { InputPasswordComponent } from "./input-password.component"; diff --git a/libs/auth/src/angular/login-approval/login-approval.component.html b/libs/auth/src/angular/login-approval/login-approval.component.html index ddbc48d71a3..c0cb9b9caf4 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.html +++ b/libs/auth/src/angular/login-approval/login-approval.component.html @@ -1,23 +1,31 @@ {{ "areYouTryingtoLogin" | i18n }} -

{{ "logInAttemptBy" | i18n: email }}

-
- {{ "fingerprintPhraseHeader" | i18n }} -

{{ fingerprintPhrase }}

-
-
- {{ "deviceType" | i18n }} -

{{ authRequestResponse?.requestDeviceType }}

-
-
- {{ "ipAddress" | i18n }} -

{{ authRequestResponse?.requestIpAddress }}

-
-
- {{ "time" | i18n }} -

{{ requestTimeText }}

-
+ +
+ +
+
+ + +

{{ "logInAttemptBy" | i18n: email }}

+
+ {{ "fingerprintPhraseHeader" | i18n }} +

{{ fingerprintPhrase }}

+
+
+ {{ "deviceType" | i18n }} +

{{ authRequestResponse?.requestDeviceType }}

+
+
+ {{ "ipAddress" | i18n }} +

{{ authRequestResponse?.requestIpAddress }}

+
+
+ {{ "time" | i18n }} +

{{ requestTimeText }}

+
+

type="button" buttonType="primary" [bitAction]="approveLogin" - [bitDialogClose]="true" + [disabled]="loading" > {{ "confirmLogIn" | i18n }} @@ -34,7 +42,7 @@

{{ "logInAttemptBy" | i18n: email }}

type="button" buttonType="secondary" [bitAction]="denyLogin" - [bitDialogClose]="true" + [disabled]="loading" > {{ "denyLogIn" | i18n }} diff --git a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts index ff598bdeb91..da30df62fff 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.spec.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.spec.ts @@ -13,6 +13,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService } from "@bitwarden/components"; import { KeyService } from "@bitwarden/key-management"; @@ -29,6 +30,7 @@ describe("LoginApprovalComponent", () => { let i18nService: MockProxy; let dialogRef: MockProxy; let toastService: MockProxy; + let validationService: MockProxy; const testNotificationId = "test-notification-id"; const testEmail = "test@bitwarden.com"; @@ -41,6 +43,7 @@ describe("LoginApprovalComponent", () => { i18nService = mock(); dialogRef = mock(); toastService = mock(); + validationService = mock(); accountService.activeAccount$ = of({ email: testEmail, @@ -62,6 +65,7 @@ describe("LoginApprovalComponent", () => { { provide: KeyService, useValue: mock() }, { provide: DialogRef, useValue: dialogRef }, { provide: ToastService, useValue: toastService }, + { provide: ValidationService, useValue: validationService }, { provide: LoginApprovalComponentServiceAbstraction, useValue: mock(), diff --git a/libs/auth/src/angular/login-approval/login-approval.component.ts b/libs/auth/src/angular/login-approval/login-approval.component.ts index 5192334a0ca..3b44f545abb 100644 --- a/libs/auth/src/angular/login-approval/login-approval.component.ts +++ b/libs/auth/src/angular/login-approval/login-approval.component.ts @@ -16,6 +16,7 @@ import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, @@ -40,6 +41,8 @@ export interface LoginApprovalDialogParams { imports: [CommonModule, AsyncActionsModule, ButtonModule, DialogModule, JslibModule], }) export class LoginApprovalComponent implements OnInit, OnDestroy { + loading = true; + notificationId: string; private destroy$ = new Subject(); @@ -62,25 +65,25 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { private dialogRef: DialogRef, private toastService: ToastService, private loginApprovalComponentService: LoginApprovalComponentService, + private validationService: ValidationService, ) { this.notificationId = params.notificationId; } async ngOnDestroy(): Promise { clearInterval(this.interval); - const closedWithButton = await firstValueFrom(this.dialogRef.closed); - if (!closedWithButton) { - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.retrieveAuthRequestAndRespond(false); - } this.destroy$.next(); this.destroy$.complete(); } async ngOnInit() { if (this.notificationId != null) { - this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + try { + this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId); + } catch (error) { + this.validationService.showError(error); + } + const publicKey = Utils.fromB64ToArray(this.authRequestResponse.publicKey); this.email = await await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), @@ -96,6 +99,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { }, RequestTimeUpdate); this.loginApprovalComponentService.showLoginRequestedAlertIfWindowNotVisible(this.email); + + this.loading = false; } } @@ -131,6 +136,8 @@ export class LoginApprovalComponent implements OnInit, OnDestroy { ); this.showResultToast(loginResponse); } + + this.dialogRef.close(approve); } showResultToast(loginResponse: AuthRequestResponse) { diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts index 5600077c363..a3f5e062e4f 100644 --- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts +++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.ts @@ -107,7 +107,7 @@ export class LoginDecryptionOptionsComponent implements OnInit { private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private validationService: ValidationService, ) { - this.clientType === this.platformUtilsService.getClientType(); + this.clientType = this.platformUtilsService.getClientType(); } async ngOnInit() { diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 40f85e6d75c..f9aaa5d1e05 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -484,6 +484,8 @@ export class LoginComponent implements OnInit, OnDestroy { try { const deviceIdentifier = await this.appIdService.getAppId(); this.isKnownDevice = await this.devicesApiService.getKnownDevice(email, deviceIdentifier); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.isKnownDevice = false; } diff --git a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts index fa3ad2ae2b9..6047cc3d27a 100644 --- a/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts +++ b/libs/auth/src/angular/registration/registration-start/registration-start.stories.ts @@ -28,7 +28,8 @@ import { } from "@bitwarden/components"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "../../../../../../apps/web/src/app/core/tests"; import { LoginEmailService } from "../../../common"; import { AnonLayoutWrapperDataService } from "../../anon-layout/anon-layout-wrapper-data.service"; diff --git a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts index 0557227938f..b54529f6a2c 100644 --- a/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts +++ b/libs/auth/src/angular/set-password-jit/set-password-jit.component.ts @@ -15,6 +15,8 @@ import { ValidationService } from "@bitwarden/common/platform/abstractions/valid import { UserId } from "@bitwarden/common/types/guid"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ToastService } from "../../../../components/src/toast"; import { InputPasswordComponent } from "../input-password/input-password.component"; import { PasswordInputResult } from "../input-password/password-input-result"; diff --git a/libs/auth/src/angular/sso/sso.component.ts b/libs/auth/src/angular/sso/sso.component.ts index aad0df4e397..3bcc56ae4cd 100644 --- a/libs/auth/src/angular/sso/sso.component.ts +++ b/libs/auth/src/angular/sso/sso.component.ts @@ -12,6 +12,7 @@ import { TrustedDeviceUserDecryptionOption, UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, + LoginSuccessHandlerService, } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; @@ -35,7 +36,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { AsyncActionsModule, ButtonModule, @@ -117,7 +117,7 @@ export class SsoComponent implements OnInit { private accountService: AccountService, private toastService: ToastService, private ssoComponentService: SsoComponentService, - private syncService: SyncService, + private loginSuccessHandlerService: LoginSuccessHandlerService, ) { environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => { this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html"; @@ -378,8 +378,7 @@ export class SsoComponent implements OnInit { // Everything after the 2FA check is considered a successful login // Just have to figure out where to send the user - - await this.syncService.fullSync(true); + await this.loginSuccessHandlerService.run(authResult.userId); // Save off the OrgSsoIdentifier for use in the TDE flows (or elsewhere) // - TDE login decryption options component diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts index 95efe2b0077..00b78064d83 100644 --- a/libs/auth/src/common/models/domain/user-decryption-options.ts +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -4,6 +4,8 @@ import { Jsonify } from "type-fest"; import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response"; import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response"; /** diff --git a/libs/auth/src/common/services/login-email/login-email.service.ts b/libs/auth/src/common/services/login-email/login-email.service.ts index feb0cba1bdc..aa13afd5004 100644 --- a/libs/auth/src/common/services/login-email/login-email.service.ts +++ b/libs/auth/src/common/services/login-email/login-email.service.ts @@ -6,6 +6,8 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { GlobalState, KeyDefinition, diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 1f98a117c88..57a653b205e 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -35,6 +35,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { MasterKey } from "@bitwarden/common/types/key"; diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts index ffb660e6a7f..7c44a6f1682 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -8,6 +8,8 @@ import { USER_DECRYPTION_OPTIONS_DISK, UserKeyDefinition, } from "@bitwarden/common/platform/state"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserId } from "@bitwarden/common/src/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; diff --git a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts index 717e80b110d..24b3adacc21 100644 --- a/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts +++ b/libs/auth/src/common/utilities/decode-jwt-token-to-json.utility.ts @@ -18,6 +18,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { try { // Attempt to decode from URL-safe Base64 to UTF-8 decodedPayloadJSON = Utils.fromUrlB64ToUtf8(encodedPayload); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (decodingError) { throw new Error("Cannot decode the token"); } @@ -26,6 +28,8 @@ export function decodeJwtTokenToJson(jwtToken: string): any { // Attempt to parse the JSON payload const decodedToken = JSON.parse(decodedPayloadJSON); return decodedToken; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (jsonError) { throw new Error("Cannot parse the token's payload into JSON"); } diff --git a/libs/auth/tsconfig.json b/libs/auth/tsconfig.json index 6004a56fb55..9be942d38de 100644 --- a/libs/auth/tsconfig.json +++ b/libs/auth/tsconfig.json @@ -1,5 +1,22 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/auth/angular": ["../auth/src/angular"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/billing/jest.config.js b/libs/billing/jest.config.js index d9bae9633ea..c43606191b9 100644 --- a/libs/billing/jest.config.js +++ b/libs/billing/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/billing/tsconfig.json b/libs/billing/tsconfig.json index 6004a56fb55..bb08eb89d1c 100644 --- a/libs/billing/tsconfig.json +++ b/libs/billing/tsconfig.json @@ -1,5 +1,6 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": {}, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/common/jest.config.js b/libs/common/jest.config.js index d7f78abbf38..7e6c0997b9c 100644 --- a/libs/common/jest.config.js +++ b/libs/common/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5ecfca5ab84..5a393ed1996 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,7 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - +import { ListResponse } from "../../../models/response/list.response"; import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response"; diff --git a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts index c4817306a63..066d2d381e7 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../../models/response/base.response"; export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse { organizationName: string; diff --git a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts index 0c45c919e95..000d1655416 100644 --- a/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization/organization-api.service.abstraction.ts @@ -1,7 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; - import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; import { OrganizationSsoRequest } from "../../../auth/models/request/organization-sso.request"; import { SecretVerificationRequest } from "../../../auth/models/request/secret-verification.request"; @@ -13,6 +11,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts index 7497a77e6f2..1052fe504d4 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts @@ -1,14 +1,13 @@ import { mock } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; -import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; -import { ListResponse } from "@bitwarden/common/models/response/list.response"; - import { ApiService } from "../../../abstractions/api.service"; +import { ListResponse } from "../../../models/response/list.response"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; import { OrgDomainApiService } from "./org-domain-api.service"; import { OrgDomainService } from "./org-domain.service"; diff --git a/libs/common/src/admin-console/services/organization/organization-api.service.ts b/libs/common/src/admin-console/services/organization/organization-api.service.ts index b3fd11982b8..598bb2a29db 100644 --- a/libs/common/src/admin-console/services/organization/organization-api.service.ts +++ b/libs/common/src/admin-console/services/organization/organization-api.service.ts @@ -1,6 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingHistoryResponse } from "@bitwarden/common/billing/models/response/billing-history.response"; import { ApiService } from "../../../abstractions/api.service"; import { OrganizationApiKeyRequest } from "../../../admin-console/models/request/organization-api-key.request"; @@ -14,6 +13,7 @@ import { OrganizationSmSubscriptionUpdateRequest } from "../../../billing/models import { OrganizationSubscriptionUpdateRequest } from "../../../billing/models/request/organization-subscription-update.request"; import { PaymentRequest } from "../../../billing/models/request/payment.request"; import { SecretsManagerSubscribeRequest } from "../../../billing/models/request/sm-subscribe.request"; +import { BillingHistoryResponse } from "../../../billing/models/response/billing-history.response"; import { BillingResponse } from "../../../billing/models/response/billing.response"; import { OrganizationSubscriptionResponse } from "../../../billing/models/response/organization-subscription.response"; import { PaymentResponse } from "../../../billing/models/response/payment.response"; diff --git a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts b/libs/common/src/admin-console/services/organization/vnext-organization.state.ts index 48e09d6d076..fea0423f389 100644 --- a/libs/common/src/admin-console/services/organization/vnext-organization.state.ts +++ b/libs/common/src/admin-console/services/organization/vnext-organization.state.ts @@ -1,7 +1,6 @@ import { Jsonify } from "type-fest"; -import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; - +import { ORGANIZATIONS_DISK, UserKeyDefinition } from "../../../platform/state"; import { OrganizationData } from "../../models/data/organization.data"; /** diff --git a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts index 0af89928449..92f0ebf1667 100644 --- a/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices-api.service.abstraction.ts @@ -36,4 +36,10 @@ export abstract class DevicesApiServiceAbstraction { * @param deviceIdentifier - current device identifier */ postDeviceTrustLoss: (deviceIdentifier: string) => Promise; + + /** + * Deactivates a device + * @param deviceId - The device ID + */ + deactivateDevice: (deviceId: string) => Promise; } diff --git a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts index a02ccc64876..ba6890947c1 100644 --- a/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/devices/devices.service.abstraction.ts @@ -1,17 +1,18 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Observable } from "rxjs"; +import { DeviceResponse } from "./responses/device.response"; import { DeviceView } from "./views/device.view"; export abstract class DevicesServiceAbstraction { - getDevices$: () => Observable>; - getDeviceByIdentifier$: (deviceIdentifier: string) => Observable; - isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable; - updateTrustedDeviceKeys$: ( + abstract getDevices$(): Observable>; + abstract getDeviceByIdentifier$(deviceIdentifier: string): Observable; + abstract isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable; + abstract updateTrustedDeviceKeys$( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, userKeyEncryptedDevicePublicKey: string, deviceKeyEncryptedDevicePrivateKey: string, - ) => Observable; + ): Observable; + abstract deactivateDevice$(deviceId: string): Observable; + abstract getCurrentDevice$(): Observable; } diff --git a/libs/common/src/auth/abstractions/devices/responses/device.response.ts b/libs/common/src/auth/abstractions/devices/responses/device.response.ts index a4e40037b05..84a2fb03c28 100644 --- a/libs/common/src/auth/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/auth/abstractions/devices/responses/device.response.ts @@ -1,6 +1,11 @@ import { DeviceType } from "../../../../enums"; import { BaseResponse } from "../../../../models/response/base.response"; +export interface DevicePendingAuthRequest { + id: string; + creationDate: string; +} + export class DeviceResponse extends BaseResponse { id: string; userId: string; @@ -9,6 +14,9 @@ export class DeviceResponse extends BaseResponse { type: DeviceType; creationDate: string; revisionDate: string; + isTrusted: boolean; + devicePendingAuthRequest: DevicePendingAuthRequest | null; + constructor(response: any) { super(response); this.id = this.getResponseProperty("Id"); @@ -18,5 +26,7 @@ export class DeviceResponse extends BaseResponse { this.type = this.getResponseProperty("Type"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); + this.isTrusted = this.getResponseProperty("IsTrusted"); + this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest"); } } diff --git a/libs/common/src/auth/abstractions/devices/views/device.view.ts b/libs/common/src/auth/abstractions/devices/views/device.view.ts index a901eb998b4..22e522b9eb0 100644 --- a/libs/common/src/auth/abstractions/devices/views/device.view.ts +++ b/libs/common/src/auth/abstractions/devices/views/device.view.ts @@ -12,6 +12,7 @@ export class DeviceView implements View { type: DeviceType; creationDate: string; revisionDate: string; + response: DeviceResponse; constructor(deviceResponse: DeviceResponse) { Object.assign(this, deviceResponse); diff --git a/libs/common/src/auth/models/request/update-devices-trust.request.ts b/libs/common/src/auth/models/request/update-devices-trust.request.ts index 8e3ce86c1a9..21fe0f600dc 100644 --- a/libs/common/src/auth/models/request/update-devices-trust.request.ts +++ b/libs/common/src/auth/models/request/update-devices-trust.request.ts @@ -8,8 +8,8 @@ export class UpdateDevicesTrustRequest extends SecretVerificationRequest { } export class DeviceKeysUpdateRequest { - encryptedPublicKey: string; - encryptedUserKey: string; + encryptedPublicKey: string | undefined; + encryptedUserKey: string | undefined; } export class OtherDeviceKeysUpdateRequest extends DeviceKeysUpdateRequest { diff --git a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts index 791c4688078..84103fe5d29 100644 --- a/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts +++ b/libs/common/src/auth/models/request/webauthn-rotate-credential.request.ts @@ -1,8 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { RotateableKeySet } from "../../../../../auth/src/common/models"; +import { EncString } from "../../../platform/models/domain/enc-string"; export class WebauthnRotateCredentialRequest { id: string; diff --git a/libs/common/src/auth/services/anonymous-hub.service.ts b/libs/common/src/auth/services/anonymous-hub.service.ts index a268c8a2712..3900dd53ee0 100644 --- a/libs/common/src/auth/services/anonymous-hub.service.ts +++ b/libs/common/src/auth/services/anonymous-hub.service.ts @@ -9,6 +9,8 @@ import { import { MessagePackHubProtocol } from "@microsoft/signalr-protocol-msgpack"; import { firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { AuthRequestServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { NotificationType } from "../../enums"; import { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 5663384714d..4b7b8a2b262 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -1,6 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index bab83fc55db..d855a1be34f 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -11,6 +11,8 @@ import { switchMap, } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { StateService } from "../../platform/abstractions/state.service"; diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts index 409150552ad..a94c8b6e422 100644 --- a/libs/common/src/auth/services/device-trust.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -4,6 +4,8 @@ import { firstValueFrom, map, Observable } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; @@ -335,6 +337,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction { ); return new SymmetricCryptoKey(userKey) as UserKey; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // If either decryption effort fails, we want to remove the device key this.logService.error("Failed to decrypt using device key. Removing device key."); diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts index 66a91a693e5..943653e3129 100644 --- a/libs/common/src/auth/services/device-trust.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -3,7 +3,11 @@ import { BehaviorSubject, of } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState } from "../../../spec/fake-state"; diff --git a/libs/common/src/auth/services/devices-api.service.implementation.spec.ts b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts new file mode 100644 index 00000000000..7aea36c7bd4 --- /dev/null +++ b/libs/common/src/auth/services/devices-api.service.implementation.spec.ts @@ -0,0 +1,100 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ApiService } from "../../abstractions/api.service"; +import { DeviceResponse } from "../abstractions/devices/responses/device.response"; + +import { DevicesApiServiceImplementation } from "./devices-api.service.implementation"; + +describe("DevicesApiServiceImplementation", () => { + let devicesApiService: DevicesApiServiceImplementation; + let apiService: MockProxy; + + beforeEach(() => { + apiService = mock(); + devicesApiService = new DevicesApiServiceImplementation(apiService); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("getKnownDevice", () => { + it("calls api with correct parameters", async () => { + const email = "test@example.com"; + const deviceIdentifier = "device123"; + apiService.send.mockResolvedValue(true); + + const result = await devicesApiService.getKnownDevice(email, deviceIdentifier); + + expect(result).toBe(true); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + "/devices/knowndevice", + null, + false, + true, + null, + expect.any(Function), + ); + }); + }); + + describe("getDeviceByIdentifier", () => { + it("returns device response", async () => { + const deviceIdentifier = "device123"; + const mockResponse = { id: "123", name: "Test Device" }; + apiService.send.mockResolvedValue(mockResponse); + + const result = await devicesApiService.getDeviceByIdentifier(deviceIdentifier); + + expect(result).toBeInstanceOf(DeviceResponse); + expect(apiService.send).toHaveBeenCalledWith( + "GET", + `/devices/identifier/${deviceIdentifier}`, + null, + true, + true, + ); + }); + }); + + describe("updateTrustedDeviceKeys", () => { + it("updates device keys and returns device response", async () => { + const deviceIdentifier = "device123"; + const publicKeyEncrypted = "encryptedPublicKey"; + const userKeyEncrypted = "encryptedUserKey"; + const deviceKeyEncrypted = "encryptedDeviceKey"; + const mockResponse = { id: "123", name: "Test Device" }; + apiService.send.mockResolvedValue(mockResponse); + + const result = await devicesApiService.updateTrustedDeviceKeys( + deviceIdentifier, + publicKeyEncrypted, + userKeyEncrypted, + deviceKeyEncrypted, + ); + + expect(result).toBeInstanceOf(DeviceResponse); + expect(apiService.send).toHaveBeenCalledWith( + "PUT", + `/devices/${deviceIdentifier}/keys`, + { + encryptedPrivateKey: deviceKeyEncrypted, + encryptedPublicKey: userKeyEncrypted, + encryptedUserKey: publicKeyEncrypted, + }, + true, + true, + ); + }); + }); + + describe("error handling", () => { + it("propagates api errors", async () => { + const error = new Error("API Error"); + apiService.send.mockRejectedValue(error); + + await expect(devicesApiService.getDevices()).rejects.toThrow("API Error"); + }); + }); +}); diff --git a/libs/common/src/auth/services/devices-api.service.implementation.ts b/libs/common/src/auth/services/devices-api.service.implementation.ts index 711f0bb68ec..cf760effbdf 100644 --- a/libs/common/src/auth/services/devices-api.service.implementation.ts +++ b/libs/common/src/auth/services/devices-api.service.implementation.ts @@ -117,4 +117,8 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac }, ); } + + async deactivateDevice(deviceId: string): Promise { + await this.apiService.send("POST", `/devices/${deviceId}/deactivate`, null, true, false); + } } diff --git a/libs/common/src/auth/services/devices/devices.service.implementation.ts b/libs/common/src/auth/services/devices/devices.service.implementation.ts index 6032ed66a89..cdaa7a9fc4e 100644 --- a/libs/common/src/auth/services/devices/devices.service.implementation.ts +++ b/libs/common/src/auth/services/devices/devices.service.implementation.ts @@ -1,6 +1,7 @@ import { Observable, defer, map } from "rxjs"; import { ListResponse } from "../../../models/response/list.response"; +import { AppIdService } from "../../../platform/abstractions/app-id.service"; import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction"; import { DeviceResponse } from "../../abstractions/devices/responses/device.response"; import { DeviceView } from "../../abstractions/devices/views/device.view"; @@ -15,7 +16,10 @@ import { DevicesApiServiceAbstraction } from "../../abstractions/devices-api.ser * (i.e., promsise --> observables are cold until subscribed to) */ export class DevicesServiceImplementation implements DevicesServiceAbstraction { - constructor(private devicesApiService: DevicesApiServiceAbstraction) {} + constructor( + private devicesApiService: DevicesApiServiceAbstraction, + private appIdService: AppIdService, + ) {} /** * @description Gets the list of all devices. @@ -65,4 +69,21 @@ export class DevicesServiceImplementation implements DevicesServiceAbstraction { ), ).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))); } + + /** + * @description Deactivates a device + */ + deactivateDevice$(deviceId: string): Observable { + return defer(() => this.devicesApiService.deactivateDevice(deviceId)); + } + + /** + * @description Gets the current device. + */ + getCurrentDevice$(): Observable { + return defer(async () => { + const deviceIdentifier = await this.appIdService.getAppId(); + return this.devicesApiService.getDeviceByIdentifier(deviceIdentifier); + }); + } } diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts index b1bf87693c1..660f1124f4a 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { ApiService } from "../../abstractions/api.service"; diff --git a/libs/common/src/auth/services/master-password/master-password.service.ts b/libs/common/src/auth/services/master-password/master-password.service.ts index ea6e1045c10..14e7522a836 100644 --- a/libs/common/src/auth/services/master-password/master-password.service.ts +++ b/libs/common/src/auth/services/master-password/master-password.service.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { firstValueFrom, map, Observable } from "rxjs"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; - import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; +import { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { EncryptionType } from "../../../platform/enums"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; @@ -180,10 +179,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr let decUserKey: Uint8Array; if (userKey.encryptionType === EncryptionType.AesCbc256_B64) { - decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + masterKey, + "Content: User Key; Encrypting Key: Master Key", + ); } else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) { const newKey = await this.keyGenerationService.stretchKey(masterKey); - decUserKey = await this.encryptService.decryptToBytes(userKey, newKey); + decUserKey = await this.encryptService.decryptToBytes( + userKey, + newKey, + "Content: User Key; Encrypting Key: Stretched Master Key", + ); } else { throw new Error("Unsupported encryption type."); } diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts index ed622b21c86..ddd24ae7907 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts @@ -2,13 +2,15 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { UserId } from "../../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; +import { UserId } from "../../types/guid"; import { Account, AccountInfo, AccountService } from "../abstractions/account.service"; import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation"; diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts index c5451ce30c6..22d5384e6ac 100644 --- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts +++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts @@ -6,10 +6,12 @@ import { OrganizationUserApiService, OrganizationUserResetPasswordEnrollmentRequest, } from "@bitwarden/admin-console/common"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { Utils } from "../../platform/misc/utils"; import { UserKey } from "../../types/key"; diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts index 4aa3a632855..102e4bac8da 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts @@ -7,14 +7,19 @@ import { UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { KdfConfig, KeyService } from "@bitwarden/key-management"; - +import { + BiometricsService, + BiometricsStatus, + KdfConfig, + KeyService, +} from "@bitwarden/key-management"; + +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec"; import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { LogService } from "../../../platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { HashPurpose } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { UserId } from "../../../types/guid"; @@ -36,10 +41,9 @@ describe("UserVerificationService", () => { const userVerificationApiService = mock(); const userDecryptionOptionsService = mock(); const pinService = mock(); - const logService = mock(); const vaultTimeoutSettingsService = mock(); - const platformUtilsService = mock(); const kdfConfigService = mock(); + const biometricsService = mock(); const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -56,10 +60,8 @@ describe("UserVerificationService", () => { userVerificationApiService, userDecryptionOptionsService, pinService, - logService, - vaultTimeoutSettingsService, - platformUtilsService, kdfConfigService, + biometricsService, ); }); @@ -113,26 +115,15 @@ describe("UserVerificationService", () => { ); test.each([ - [true, true, true, true], - [true, true, true, false], - [true, true, false, false], - [false, true, false, true], - [false, false, false, false], - [false, false, true, false], - [false, false, false, true], + [true, BiometricsStatus.Available], + [false, BiometricsStatus.DesktopDisconnected], + [false, BiometricsStatus.HardwareUnavailable], ])( "returns %s for biometrics availability when isBiometricLockSet is %s, hasUserKeyStored is %s, and supportsSecureStorage is %s", - async ( - expectedReturn: boolean, - isBiometricsLockSet: boolean, - isBiometricsUserKeyStored: boolean, - platformSupportSecureStorage: boolean, - ) => { + async (expectedReturn: boolean, biometricsStatus: BiometricsStatus) => { setMasterPasswordAvailability(false); setPinAvailability("DISABLED"); - vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(isBiometricsLockSet); - keyService.hasUserKeyStored.mockResolvedValue(isBiometricsUserKeyStored); - platformUtilsService.supportsSecureStorage.mockReturnValue(platformSupportSecureStorage); + biometricsService.getBiometricsStatus.mockResolvedValue(biometricsStatus); const result = await sut.getAvailableVerificationOptions("client"); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 822ee70ec5b..4735da32b6b 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -3,17 +3,19 @@ import { firstValueFrom, map } from "rxjs"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; -import { KdfConfigService, KeyService } from "@bitwarden/key-management"; - +import { + BiometricsService, + BiometricsStatus, + KdfConfigService, + KeyService, +} from "@bitwarden/key-management"; + +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction"; -import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; -import { LogService } from "../../../platform/abstractions/log.service"; -import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { HashPurpose } from "../../../platform/enums"; -import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enum"; import { UserId } from "../../../types/guid"; -import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; @@ -47,10 +49,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private userVerificationApiService: UserVerificationApiServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private pinService: PinServiceAbstraction, - private logService: LogService, - private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, - private platformUtilsService: PlatformUtilsService, private kdfConfigService: KdfConfigService, + private biometricsService: BiometricsService, ) {} async getAvailableVerificationOptions( @@ -58,17 +58,13 @@ export class UserVerificationService implements UserVerificationServiceAbstracti ): Promise { const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (verificationType === "client") { - const [ - userHasMasterPassword, - isPinDecryptionAvailable, - biometricsLockSet, - biometricsUserKeyStored, - ] = await Promise.all([ - this.hasMasterPasswordAndMasterKeyHash(userId), - this.pinService.isPinDecryptionAvailable(userId), - this.vaultTimeoutSettingsService.isBiometricLockSet(userId), - this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric, userId), - ]); + const [userHasMasterPassword, isPinDecryptionAvailable, biometricsStatus] = await Promise.all( + [ + this.hasMasterPasswordAndMasterKeyHash(userId), + this.pinService.isPinDecryptionAvailable(userId), + this.biometricsService.getBiometricsStatus(), + ], + ); // note: we do not need to check this.platformUtilsService.supportsBiometric() because // we can just use the logic below which works for both desktop & the browser extension. @@ -77,9 +73,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti client: { masterPassword: userHasMasterPassword, pin: isPinDecryptionAvailable, - biometrics: - biometricsLockSet && - (biometricsUserKeyStored || !this.platformUtilsService.supportsSecureStorage()), + biometrics: biometricsStatus === BiometricsStatus.Available, }, server: { masterPassword: false, @@ -169,6 +163,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti const request = new VerifyOTPRequest(verification.secret); try { await this.userVerificationApiService.postAccountVerifyOTP(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidVerificationCode")); } @@ -227,6 +223,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti request.masterPasswordHash = serverKeyHash; try { policyOptions = await this.userVerificationApiService.postAccountVerifyPassword(request); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { throw new Error(this.i18nService.t("invalidMasterPassword")); } @@ -253,17 +251,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti } private async verifyUserByBiometrics(): Promise { - let userKey: UserKey; - // Biometrics crashes and doesn't return a value if the user cancels the prompt - try { - userKey = await this.keyService.getUserKeyFromStorage(KeySuffixOptions.Biometric); - } catch (e) { - this.logService.error(`Biometrics User Verification failed: ${e.message}`); - // So, any failures should be treated as a failed verification - return false; - } - - return userKey != null; + return this.biometricsService.authenticateWithBiometrics(); } async requestOTP() { diff --git a/libs/common/src/autofill/services/domain-settings.service.spec.ts b/libs/common/src/autofill/services/domain-settings.service.spec.ts index a25653f168c..36f7d0eacec 100644 --- a/libs/common/src/autofill/services/domain-settings.service.spec.ts +++ b/libs/common/src/autofill/services/domain-settings.service.spec.ts @@ -1,9 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; - import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; @@ -29,7 +28,7 @@ describe("DefaultDomainSettingsService", () => { jest.spyOn(domainSettingsService, "getUrlEquivalentDomains"); domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); - domainSettingsService.blockedInteractionsUris$ = of(null); + domainSettingsService.blockedInteractionsUris$ = of({}); }); describe("getUrlEquivalentDomains", () => { diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts index aeb3af69dae..b2833b9ee25 100644 --- a/libs/common/src/autofill/services/domain-settings.service.ts +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -2,8 +2,7 @@ // @ts-strict-ignore import { map, Observable, switchMap, of } from "rxjs"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; - +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { NeverDomains, EquivalentDomains, @@ -36,7 +35,7 @@ const BLOCKED_INTERACTIONS_URIS = new KeyDefinition( DOMAIN_SETTINGS_DISK, "blockedInteractionsUris", { - deserializer: (value: NeverDomains) => value ?? null, + deserializer: (value: NeverDomains) => value ?? {}, }, ); @@ -131,7 +130,7 @@ export class DefaultDomainSettingsService implements DomainSettingsService { switchMap((featureIsEnabled) => featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains), ), - map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : null)), + map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : {})), ); this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS); diff --git a/libs/common/src/autofill/utils.spec.ts b/libs/common/src/autofill/utils.spec.ts index 516a09e03d1..554dc973b48 100644 --- a/libs/common/src/autofill/utils.spec.ts +++ b/libs/common/src/autofill/utils.spec.ts @@ -1,9 +1,6 @@ -import { - normalizeExpiryYearFormat, - isCardExpired, - parseYearMonthExpiry, -} from "@bitwarden/common/autofill/utils"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +import { CardView } from "../vault/models/view/card.view"; + +import { normalizeExpiryYearFormat, isCardExpired, parseYearMonthExpiry } from "./utils"; function getExpiryYearValueFormats(currentCentury: string) { return [ @@ -93,7 +90,7 @@ function getCardExpiryDateValues() { [undefined, undefined, false], // no month, no year, invalid values ["", "", false], // no month, no year, invalid values ["12", "agdredg42grg35grrr. ea3534@#^145345ag$%^ -_#$rdg ", false], // invalid values - ["0", `${currentYear}`, true], // invalid month + ["0", `${currentYear}`, false], // invalid month ["0", `${currentYear - 1}`, true], // invalid 0 month ["00", `${currentYear + 1}`, false], // invalid 0 month [`${currentMonth}`, "0000", true], // current month, in the year 2000 diff --git a/libs/common/src/autofill/utils.ts b/libs/common/src/autofill/utils.ts index d9276cdbc8b..a77ea8a715d 100644 --- a/libs/common/src/autofill/utils.ts +++ b/libs/common/src/autofill/utils.ts @@ -1,13 +1,12 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore +import { CardView } from "../vault/models/view/card.view"; + import { DelimiterPatternExpression, ExpiryFullYearPattern, ExpiryFullYearPatternExpression, IrrelevantExpiryCharactersPatternExpression, MonthPatternExpression, -} from "@bitwarden/common/autofill/constants"; -import { CardView } from "@bitwarden/common/vault/models/view/card.view"; +} from "./constants"; type NonZeroIntegers = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; type Year = `${NonZeroIntegers}${NonZeroIntegers}${0 | NonZeroIntegers}${0 | NonZeroIntegers}`; @@ -25,11 +24,11 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu let expirationYear = yearInputIsEmpty ? null : `${yearInput}`; // Exit early if year is already formatted correctly or empty - if (yearInputIsEmpty || /^[1-9]{1}\d{3}$/.test(expirationYear)) { + if (yearInputIsEmpty || (expirationYear && /^[1-9]{1}\d{3}$/.test(expirationYear))) { return expirationYear as Year; } - expirationYear = expirationYear + expirationYear = (expirationYear || "") // For safety, because even input[type="number"] will allow decimals .replace(/[^\d]/g, "") // remove any leading zero padding (leave the last leading zero if it ends the string) @@ -53,7 +52,7 @@ export function normalizeExpiryYearFormat(yearInput: string | number): Year | nu /** * Takes a cipher card view and returns "true" if the month and year affirmativey indicate - * the card is expired. + * the card is expired. Uncertain cases return "false". * * @param {CardView} cipherCard * @return {*} {boolean} @@ -62,30 +61,34 @@ export function isCardExpired(cipherCard: CardView): boolean { if (cipherCard) { const { expMonth = null, expYear = null } = cipherCard; + if (!expYear) { + return false; + } + const now = new Date(); const normalizedYear = normalizeExpiryYearFormat(expYear); - const parsedYear = parseInt(normalizedYear, 10); + const parsedYear = normalizedYear ? parseInt(normalizedYear, 10) : NaN; - const expiryYearIsBeforeThisYear = parsedYear < now.getFullYear(); - const expiryYearIsAfterThisYear = parsedYear > now.getFullYear(); + const expiryYearIsBeforeCurrentYear = parsedYear < now.getFullYear(); + const expiryYearIsAfterCurrentYear = parsedYear > now.getFullYear(); // If the expiry year is before the current year, skip checking the month, since it must be expired - if (normalizedYear && expiryYearIsBeforeThisYear) { + if (normalizedYear && expiryYearIsBeforeCurrentYear) { return true; } // If the expiry year is after the current year, skip checking the month, since it cannot be expired - if (normalizedYear && expiryYearIsAfterThisYear) { + if (normalizedYear && expiryYearIsAfterCurrentYear) { return false; } if (normalizedYear && expMonth) { const parsedMonthInteger = parseInt(expMonth, 10); - const parsedMonthIsInvalid = !parsedMonthInteger || isNaN(parsedMonthInteger); + const parsedMonthIsValid = parsedMonthInteger && !isNaN(parsedMonthInteger); - // If the parsed month value is 0, we don't know when the expiry passes this year, so treat it as expired - if (parsedMonthIsInvalid) { - return true; + // If the parsed month value is 0, we don't know when the expiry passes this year, so do not treat it as expired + if (!parsedMonthIsValid) { + return false; } // `Date` months are zero-indexed @@ -257,13 +260,18 @@ function parseNonDelimitedYearMonthExpiry(dateInput: string): [string | null, st parsedMonth = dateInput.slice(-1); const currentYear = new Date().getFullYear(); - const normalizedParsedYear = parseInt(normalizeExpiryYearFormat(parsedYear), 10); - const normalizedParsedYearAlternative = parseInt( - normalizeExpiryYearFormat(dateInput.slice(-2)), - 10, - ); - - if (normalizedParsedYear < currentYear && normalizedParsedYearAlternative >= currentYear) { + const normalizedYearFormat = normalizeExpiryYearFormat(parsedYear); + const normalizedParsedYear = normalizedYearFormat && parseInt(normalizedYearFormat, 10); + const normalizedExpiryYearFormat = normalizeExpiryYearFormat(dateInput.slice(-2)); + const normalizedParsedYearAlternative = + normalizedExpiryYearFormat && parseInt(normalizedExpiryYearFormat, 10); + + if ( + normalizedParsedYear && + normalizedParsedYear < currentYear && + normalizedParsedYearAlternative && + normalizedParsedYearAlternative >= currentYear + ) { parsedYear = dateInput.slice(-2); parsedMonth = dateInput.slice(0, 1); } @@ -295,17 +303,24 @@ export function parseYearMonthExpiry(combinedExpiryValue: string): [Year | null, // If there is only one date part, no delimiter was found in the passed value if (dateParts.length === 1) { - [parsedYear, parsedMonth] = parseNonDelimitedYearMonthExpiry(sanitizedFirstPart); + const [parsedNonDelimitedYear, parsedNonDelimitedMonth] = + parseNonDelimitedYearMonthExpiry(sanitizedFirstPart); + + parsedYear = parsedNonDelimitedYear; + parsedMonth = parsedNonDelimitedMonth; } // There are multiple date parts else { - [parsedYear, parsedMonth] = parseDelimitedYearMonthExpiry([ + const [parsedDelimitedYear, parsedDelimitedMonth] = parseDelimitedYearMonthExpiry([ sanitizedFirstPart, sanitizedSecondPart, ]); + + parsedYear = parsedDelimitedYear; + parsedMonth = parsedDelimitedMonth; } - const normalizedParsedYear = normalizeExpiryYearFormat(parsedYear); + const normalizedParsedYear = parsedYear ? normalizeExpiryYearFormat(parsedYear) : null; const normalizedParsedMonth = parsedMonth?.replace(/^0+/, "").slice(0, 2); // Set "empty" values to null diff --git a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts index 06400081e11..e0e8b7377c5 100644 --- a/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/account/account-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class AccountBillingApiServiceAbstraction { getBillingInvoices: (status?: string, startAfter?: string) => Promise; diff --git a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts index 8fbbc7c1c91..a4253226880 100644 --- a/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/abstractions/account/billing-account-profile-state.service.ts @@ -11,27 +11,32 @@ export type BillingAccountProfile = { export abstract class BillingAccountProfileStateService { /** - * Emits `true` when the active user's account has been granted premium from any of the + * Emits `true` when the user's account has been granted premium from any of the * organizations it is a member of. Otherwise, emits `false` */ - hasPremiumFromAnyOrganization$: Observable; + abstract hasPremiumFromAnyOrganization$(userId: UserId): Observable; /** - * Emits `true` when the active user's account has an active premium subscription at the + * Emits `true` when the user's account has an active premium subscription at the * individual user level */ - hasPremiumPersonally$: Observable; + abstract hasPremiumPersonally$(userId: UserId): Observable; /** * Emits `true` when either `hasPremiumPersonally` or `hasPremiumFromAnyOrganization` is `true` */ - hasPremiumFromAnySource$: Observable; + abstract hasPremiumFromAnySource$(userId: UserId): Observable; /** - * Sets the active user's premium status fields upon every full sync, either from their personal + * Emits `true` when the subscription menu item should be shown in navigation. + * This is hidden for organizations that provide premium, except if the user has premium personally + * or has a billing history. + */ + abstract canViewSubscription$(userId: UserId): Observable; + + /** + * Sets the user's premium status fields upon every full sync, either from their personal * subscription to premium, or an organization they're a part of that grants them premium. - * @param hasPremiumPersonally - * @param hasPremiumFromAnyOrganization */ abstract setHasPremium( hasPremiumPersonally: boolean, diff --git a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts index 8b82795fb50..928f65a3636 100644 --- a/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billing-api.service.abstraction.ts @@ -1,19 +1,20 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { PlanResponse } from "../../billing/models/response/plan.response"; import { ListResponse } from "../../models/response/list.response"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -74,4 +75,9 @@ export abstract class BillingApiServiceAbstraction { organizationId: string, request: VerifyBankAccountRequest, ) => Promise; + + restartSubscription: ( + organizationId: string, + request: OrganizationCreateRequest, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organization-billing.service.ts b/libs/common/src/billing/abstractions/organization-billing.service.ts index ddcd61573a6..1e68488ac98 100644 --- a/libs/common/src/billing/abstractions/organization-billing.service.ts +++ b/libs/common/src/billing/abstractions/organization-billing.service.ts @@ -1,11 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; import { InitiationPath } from "../../models/request/reference-event.request"; import { PaymentMethodType, PlanType } from "../enums"; +import { BillingSourceResponse } from "../models/response/billing.response"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; export type OrganizationInformation = { name: string; @@ -57,4 +57,9 @@ export abstract class OrganizationBillingServiceAbstraction { ) => Promise; startFree: (subscription: SubscriptionInformation) => Promise; + + restartSubscription: ( + organizationId: string, + subscription: SubscriptionInformation, + ) => Promise; } diff --git a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts index 6a72724a5ec..2ed25491049 100644 --- a/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/organizations/organization-billing-api.service.abstraction.ts @@ -3,7 +3,7 @@ import { BillingInvoiceResponse, BillingTransactionResponse, -} from "@bitwarden/common/billing/models/response/billing.response"; +} from "../../models/response/billing.response"; export class OrganizationBillingApiServiceAbstraction { getBillingInvoices: ( diff --git a/libs/common/src/billing/abstractions/tax.service.abstraction.ts b/libs/common/src/billing/abstractions/tax.service.abstraction.ts index 438d3f394e0..7a744dae856 100644 --- a/libs/common/src/billing/abstractions/tax.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/tax.service.abstraction.ts @@ -1,7 +1,7 @@ -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; export abstract class TaxServiceAbstraction { abstract getCountries(): CountryListItem[]; diff --git a/libs/common/src/billing/models/domain/tax-information.ts b/libs/common/src/billing/models/domain/tax-information.ts index 78e1bcc42b5..794cdef3ed4 100644 --- a/libs/common/src/billing/models/domain/tax-information.ts +++ b/libs/common/src/billing/models/domain/tax-information.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; +import { TaxInfoResponse } from "../response/tax-info.response"; export class TaxInformation { country: string; diff --git a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts index 784d2691629..83b254ac512 100644 --- a/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts +++ b/libs/common/src/billing/models/request/expanded-tax-info-update.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { TaxInformation } from "@bitwarden/common/billing/models/domain/tax-information"; +import { TaxInformation } from "../domain/tax-information"; import { TaxInfoUpdateRequest } from "./tax-info-update.request"; diff --git a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts index 365dff5c110..40d8db03d3b 100644 --- a/libs/common/src/billing/models/request/preview-organization-invoice.request.ts +++ b/libs/common/src/billing/models/request/preview-organization-invoice.request.ts @@ -1,4 +1,4 @@ -import { PlanType } from "@bitwarden/common/billing/enums"; +import { PlanType } from "../../enums"; export class PreviewOrganizationInvoiceRequest { organizationId?: string; diff --git a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts index c740e4157ed..e4bf575cc6a 100644 --- a/libs/common/src/billing/models/request/tokenized-payment-source.request.ts +++ b/libs/common/src/billing/models/request/tokenized-payment-source.request.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; +import { PaymentMethodType } from "../../enums"; export class TokenizedPaymentSourceRequest { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/request/update-payment-method.request.ts b/libs/common/src/billing/models/request/update-payment-method.request.ts index 9ef91ae579b..10b03103716 100644 --- a/libs/common/src/billing/models/request/update-payment-method.request.ts +++ b/libs/common/src/billing/models/request/update-payment-method.request.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ExpandedTaxInfoUpdateRequest } from "@bitwarden/common/billing/models/request/expanded-tax-info-update.request"; -import { TokenizedPaymentSourceRequest } from "@bitwarden/common/billing/models/request/tokenized-payment-source.request"; +import { ExpandedTaxInfoUpdateRequest } from "./expanded-tax-info-update.request"; +import { TokenizedPaymentSourceRequest } from "./tokenized-payment-source.request"; export class UpdatePaymentMethodRequest { paymentSource: TokenizedPaymentSourceRequest; diff --git a/libs/common/src/billing/models/response/invoices.response.ts b/libs/common/src/billing/models/response/invoices.response.ts index bf797ba42d6..05c95b83c6f 100644 --- a/libs/common/src/billing/models/response/invoices.response.ts +++ b/libs/common/src/billing/models/response/invoices.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class InvoicesResponse extends BaseResponse { invoices: InvoiceResponse[] = []; diff --git a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts index c5023cb64c1..d30ad76a147 100644 --- a/libs/common/src/billing/models/response/organization-billing-metadata.response.ts +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -10,6 +10,7 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { invoiceDueDate: Date | null; invoiceCreatedDate: Date | null; subPeriodEndDate: Date | null; + isSubscriptionCanceled: boolean; constructor(response: any) { super(response); @@ -23,6 +24,7 @@ export class OrganizationBillingMetadataResponse extends BaseResponse { this.invoiceDueDate = this.parseDate(this.getResponseProperty("InvoiceDueDate")); this.invoiceCreatedDate = this.parseDate(this.getResponseProperty("InvoiceCreatedDate")); this.subPeriodEndDate = this.parseDate(this.getResponseProperty("SubPeriodEndDate")); + this.isSubscriptionCanceled = this.getResponseProperty("IsSubscriptionCanceled"); } private parseDate(dateString: any): Date | null { diff --git a/libs/common/src/billing/models/response/payment-source.response.ts b/libs/common/src/billing/models/response/payment-source.response.ts index 1aeeb450b11..93418fc2f55 100644 --- a/libs/common/src/billing/models/response/payment-source.response.ts +++ b/libs/common/src/billing/models/response/payment-source.response.ts @@ -1,5 +1,5 @@ -import { PaymentMethodType } from "@bitwarden/common/billing/enums"; -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; +import { PaymentMethodType } from "../../enums"; export class PaymentSourceResponse extends BaseResponse { type: PaymentMethodType; diff --git a/libs/common/src/billing/models/response/preview-invoice.response.ts b/libs/common/src/billing/models/response/preview-invoice.response.ts index c822a569bb3..efd3da3e9f1 100644 --- a/libs/common/src/billing/models/response/preview-invoice.response.ts +++ b/libs/common/src/billing/models/response/preview-invoice.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class PreviewInvoiceResponse extends BaseResponse { effectiveTaxRate: number; diff --git a/libs/common/src/billing/models/response/provider-subscription-response.ts b/libs/common/src/billing/models/response/provider-subscription-response.ts index 2ecf988addd..4481f7588ff 100644 --- a/libs/common/src/billing/models/response/provider-subscription-response.ts +++ b/libs/common/src/billing/models/response/provider-subscription-response.ts @@ -1,9 +1,9 @@ -import { ProviderType } from "@bitwarden/common/admin-console/enums"; -import { PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; -import { SubscriptionSuspensionResponse } from "@bitwarden/common/billing/models/response/subscription-suspension.response"; -import { TaxInfoResponse } from "@bitwarden/common/billing/models/response/tax-info.response"; - +import { ProviderType } from "../../../admin-console/enums"; import { BaseResponse } from "../../../models/response/base.response"; +import { PlanType, ProductTierType } from "../../enums"; + +import { SubscriptionSuspensionResponse } from "./subscription-suspension.response"; +import { TaxInfoResponse } from "./tax-info.response"; export class ProviderSubscriptionResponse extends BaseResponse { status: string; diff --git a/libs/common/src/billing/models/response/subscription-suspension.response.ts b/libs/common/src/billing/models/response/subscription-suspension.response.ts index 418e1c443c8..3d714a05dba 100644 --- a/libs/common/src/billing/models/response/subscription-suspension.response.ts +++ b/libs/common/src/billing/models/response/subscription-suspension.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class SubscriptionSuspensionResponse extends BaseResponse { suspensionDate: string; diff --git a/libs/common/src/billing/models/response/tax-id-types.response.ts b/libs/common/src/billing/models/response/tax-id-types.response.ts index 0d5cce46c8c..f31f2133b34 100644 --- a/libs/common/src/billing/models/response/tax-id-types.response.ts +++ b/libs/common/src/billing/models/response/tax-id-types.response.ts @@ -1,4 +1,4 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; +import { BaseResponse } from "../../../models/response/base.response"; export class TaxIdTypesResponse extends BaseResponse { taxIdTypes: TaxIdTypeResponse[] = []; diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts index 7e0dee0eedf..02dbef469d6 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.spec.ts @@ -6,8 +6,11 @@ import { FakeStateProvider, FakeSingleUserState, } from "../../../../spec"; +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile } from "../../abstractions/account/billing-account-profile-state.service"; +import { BillingHistoryResponse } from "../../models/response/billing-history.response"; import { BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, @@ -19,14 +22,26 @@ describe("BillingAccountProfileStateService", () => { let sut: DefaultBillingAccountProfileStateService; let userBillingAccountProfileState: FakeSingleUserState; let accountService: FakeAccountService; + let platformUtilsService: jest.Mocked; + let apiService: jest.Mocked; const userId = "fakeUserId" as UserId; beforeEach(() => { accountService = mockAccountServiceWith(userId); stateProvider = new FakeStateProvider(accountService); - - sut = new DefaultBillingAccountProfileStateService(stateProvider); + platformUtilsService = { + isSelfHost: jest.fn(), + } as any; + apiService = { + getUserBillingHistory: jest.fn(), + } as any; + + sut = new DefaultBillingAccountProfileStateService( + stateProvider, + platformUtilsService, + apiService, + ); userBillingAccountProfileState = stateProvider.singleUser.getFake( userId, @@ -45,7 +60,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(true); }); it("return false when they do not have premium from an organization", async () => { @@ -54,13 +69,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); - }); - - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); - - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false); }); }); @@ -71,7 +80,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true); }); it("returns false when the user does not have premium personally", async () => { @@ -80,13 +89,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); - }); - - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); - - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(false); }); }); @@ -97,7 +100,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: false, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); it("returns true when the user has premium from an organization", async () => { @@ -106,7 +109,7 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); it("returns true when they have premium personally AND from an organization", async () => { @@ -115,23 +118,87 @@ describe("BillingAccountProfileStateService", () => { hasPremiumFromAnyOrganization: true, }); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); + }); - it("returns false when there is no active user", async () => { - await accountService.switchAccount(null); + describe("setHasPremium", () => { + it("should update the user's state when called", async () => { + await sut.setHasPremium(true, false, userId); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(false); + expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$(userId))).toBe(false); + expect(await firstValueFrom(sut.hasPremiumPersonally$(userId))).toBe(true); + expect(await firstValueFrom(sut.hasPremiumFromAnySource$(userId))).toBe(true); }); }); - describe("setHasPremium", () => { - it("should update the active users state when called", async () => { - await sut.setHasPremium(true, false, userId); + describe("canViewSubscription$", () => { + beforeEach(() => { + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ invoices: [], transactions: [] }), + ); + }); + + it("returns true when user has premium personally", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: true, + hasPremiumFromAnyOrganization: true, + }); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns true when user has no premium from any source", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: false, + }); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns true when user has billing history in cloud environment", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ + invoices: [{ id: "1" }], + transactions: [{ id: "2" }], + }), + ); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(true); + }); + + it("returns false when user has no premium personally, has org premium, and no billing history", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(false); + apiService.getUserBillingHistory.mockResolvedValue( + new BillingHistoryResponse({ + invoices: [], + transactions: [], + }), + ); + + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false); + }); + + it("returns false when user has no premium personally, has org premium, in self-hosted environment", async () => { + userBillingAccountProfileState.nextState({ + hasPremiumPersonally: false, + hasPremiumFromAnyOrganization: true, + }); + platformUtilsService.isSelfHost.mockReturnValue(true); - expect(await firstValueFrom(sut.hasPremiumFromAnyOrganization$)).toBe(false); - expect(await firstValueFrom(sut.hasPremiumPersonally$)).toBe(true); - expect(await firstValueFrom(sut.hasPremiumFromAnySource$)).toBe(true); + expect(await firstValueFrom(sut.canViewSubscription$(userId))).toBe(false); + expect(apiService.getUserBillingHistory).not.toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts index 7d256da9714..155ce1493b4 100644 --- a/libs/common/src/billing/services/account/billing-account-profile-state.service.ts +++ b/libs/common/src/billing/services/account/billing-account-profile-state.service.ts @@ -1,11 +1,8 @@ -import { map, Observable, of, switchMap } from "rxjs"; +import { map, Observable, combineLatest, concatMap } from "rxjs"; -import { - ActiveUserState, - BILLING_DISK, - StateProvider, - UserKeyDefinition, -} from "../../../platform/state"; +import { ApiService } from "../../../abstractions/api.service"; +import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; +import { BILLING_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state"; import { UserId } from "../../../types/guid"; import { BillingAccountProfile, @@ -22,42 +19,34 @@ export const BILLING_ACCOUNT_PROFILE_KEY_DEFINITION = new UserKeyDefinition; - - hasPremiumFromAnyOrganization$: Observable; - hasPremiumPersonally$: Observable; - hasPremiumFromAnySource$: Observable; - - constructor(private readonly stateProvider: StateProvider) { - this.billingAccountProfileState = stateProvider.getActive( - BILLING_ACCOUNT_PROFILE_KEY_DEFINITION, - ); + constructor( + private readonly stateProvider: StateProvider, + private readonly platformUtilsService: PlatformUtilsService, + private readonly apiService: ApiService, + ) {} - // Setup an observable that will always track the currently active user - // but will fallback to emitting null when there is no active user. - const billingAccountProfileOrNull = stateProvider.activeUserId$.pipe( - switchMap((userId) => - userId != null - ? stateProvider.getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION).state$ - : of(null), - ), - ); - - this.hasPremiumFromAnyOrganization$ = billingAccountProfileOrNull.pipe( - map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumFromAnyOrganization), - ); + hasPremiumFromAnyOrganization$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe(map((profile) => !!profile?.hasPremiumFromAnyOrganization)); + } - this.hasPremiumPersonally$ = billingAccountProfileOrNull.pipe( - map((billingAccountProfile) => !!billingAccountProfile?.hasPremiumPersonally), - ); + hasPremiumPersonally$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe(map((profile) => !!profile?.hasPremiumPersonally)); + } - this.hasPremiumFromAnySource$ = billingAccountProfileOrNull.pipe( - map( - (billingAccountProfile) => - billingAccountProfile?.hasPremiumFromAnyOrganization === true || - billingAccountProfile?.hasPremiumPersonally === true, - ), - ); + hasPremiumFromAnySource$(userId: UserId): Observable { + return this.stateProvider + .getUser(userId, BILLING_ACCOUNT_PROFILE_KEY_DEFINITION) + .state$.pipe( + map( + (profile) => + profile?.hasPremiumFromAnyOrganization === true || + profile?.hasPremiumPersonally === true, + ), + ); } async setHasPremium( @@ -72,4 +61,23 @@ export class DefaultBillingAccountProfileStateService implements BillingAccountP }; }); } + + canViewSubscription$(userId: UserId): Observable { + return combineLatest([ + this.hasPremiumPersonally$(userId), + this.hasPremiumFromAnyOrganization$(userId), + ]).pipe( + concatMap(async ([hasPremiumPersonally, hasPremiumFromOrg]) => { + const isCloud = !this.platformUtilsService.isSelfHost(); + + let billing = null; + if (isCloud) { + billing = await this.apiService.getUserBillingHistory(); + } + + const cloudAndBillingHistory = isCloud && !billing?.hasNoHistory; + return hasPremiumPersonally || !hasPremiumFromOrg || cloudAndBillingHistory; + }), + ); + } } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index cb69f294409..4306324395e 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,24 +1,25 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; -import { UpdatePaymentMethodRequest } from "@bitwarden/common/billing/models/request/update-payment-method.request"; -import { VerifyBankAccountRequest } from "@bitwarden/common/billing/models/request/verify-bank-account.request"; -import { InvoicesResponse } from "@bitwarden/common/billing/models/response/invoices.response"; -import { PaymentMethodResponse } from "@bitwarden/common/billing/models/response/payment-method.response"; -import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ToastService } from "@bitwarden/components"; import { ApiService } from "../../abstractions/api.service"; -import { BillingApiServiceAbstraction } from "../../billing/abstractions"; -import { PaymentMethodType } from "../../billing/enums"; -import { ExpandedTaxInfoUpdateRequest } from "../../billing/models/request/expanded-tax-info-update.request"; -import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; -import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; -import { PlanResponse } from "../../billing/models/response/plan.response"; +import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; +import { ProviderOrganizationOrganizationDetailsResponse } from "../../admin-console/models/response/provider/provider-organization.response"; +import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; +import { LogService } from "../../platform/abstractions/log.service"; +import { BillingApiServiceAbstraction } from "../abstractions"; +import { PaymentMethodType } from "../enums"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { ExpandedTaxInfoUpdateRequest } from "../models/request/expanded-tax-info-update.request"; +import { SubscriptionCancellationRequest } from "../models/request/subscription-cancellation.request"; import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; +import { UpdatePaymentMethodRequest } from "../models/request/update-payment-method.request"; +import { VerifyBankAccountRequest } from "../models/request/verify-bank-account.request"; +import { InvoicesResponse } from "../models/response/invoices.response"; +import { OrganizationBillingMetadataResponse } from "../models/response/organization-billing-metadata.response"; +import { PaymentMethodResponse } from "../models/response/payment-method.response"; +import { PlanResponse } from "../models/response/plan.response"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { @@ -214,6 +215,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { ); } + async restartSubscription( + organizationId: string, + request: OrganizationCreateRequest, + ): Promise { + return await this.apiService.send( + "POST", + "/organizations/" + organizationId + "/billing/restart-subscription", + request, + true, + false, + ); + } + private async execute(request: () => Promise): Promise { try { return await request(); diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index a0b3782f1ad..da1a1666ff0 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,18 +1,5 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { - BillingApiServiceAbstraction, - OrganizationBillingServiceAbstraction, - OrganizationInformation, - PaymentInformation, - PlanInformation, - SubscriptionInformation, -} from "@bitwarden/common/billing/abstractions"; -import { BillingSourceResponse } from "@bitwarden/common/billing/models/response/billing.response"; -import { PaymentSourceResponse } from "@bitwarden/common/billing/models/response/payment-source.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { SyncService } from "@bitwarden/common/platform/sync"; import { KeyService } from "@bitwarden/key-management"; import { ApiService } from "../../abstractions/api.service"; @@ -20,12 +7,25 @@ import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../ import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; import { OrganizationResponse } from "../../admin-console/models/response/organization.response"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; +import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { SyncService } from "../../platform/sync"; import { OrgKey } from "../../types/key"; +import { + BillingApiServiceAbstraction, + OrganizationBillingServiceAbstraction, + OrganizationInformation, + PaymentInformation, + PlanInformation, + SubscriptionInformation, +} from "../abstractions"; import { PlanType } from "../enums"; import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request"; +import { BillingSourceResponse } from "../models/response/billing.response"; +import { PaymentSourceResponse } from "../models/response/payment-source.response"; interface OrganizationKeys { encryptedKey: EncString; @@ -223,4 +223,17 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs request.additionalStorageGb = information.storage; } } + + async restartSubscription( + organizationId: string, + subscription: SubscriptionInformation, + ): Promise { + const request = new OrganizationCreateRequest(); + const organizationKeys = await this.makeOrganizationKeys(); + this.setOrganizationKeys(request, organizationKeys); + this.setOrganizationInformation(request, subscription.organization); + this.setPlanInformation(request, subscription.plan); + this.setPaymentInformation(request, subscription.payment); + await this.billingApiService.restartSubscription(organizationId, request); + } } diff --git a/libs/common/src/billing/services/tax.service.ts b/libs/common/src/billing/services/tax.service.ts index 45e57267ec0..aa27c99adc8 100644 --- a/libs/common/src/billing/services/tax.service.ts +++ b/libs/common/src/billing/services/tax.service.ts @@ -1,9 +1,9 @@ -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { TaxServiceAbstraction } from "@bitwarden/common/billing/abstractions/tax.service.abstraction"; -import { CountryListItem } from "@bitwarden/common/billing/models/domain"; -import { PreviewIndividualInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-individual-invoice.request"; -import { PreviewOrganizationInvoiceRequest } from "@bitwarden/common/billing/models/request/preview-organization-invoice.request"; -import { PreviewInvoiceResponse } from "@bitwarden/common/billing/models/response/preview-invoice.response"; +import { ApiService } from "../../abstractions/api.service"; +import { TaxServiceAbstraction } from "../abstractions/tax.service.abstraction"; +import { CountryListItem } from "../models/domain"; +import { PreviewIndividualInvoiceRequest } from "../models/request/preview-individual-invoice.request"; +import { PreviewOrganizationInvoiceRequest } from "../models/request/preview-organization-invoice.request"; +import { PreviewInvoiceResponse } from "../models/response/preview-invoice.response"; export class TaxService implements TaxServiceAbstraction { constructor(private apiService: ApiService) {} diff --git a/libs/common/src/enums/device-type.enum.ts b/libs/common/src/enums/device-type.enum.ts index 1b8574a4c49..ff6329b9ac4 100644 --- a/libs/common/src/enums/device-type.enum.ts +++ b/libs/common/src/enums/device-type.enum.ts @@ -27,18 +27,40 @@ export enum DeviceType { LinuxCLI = 25, } -export const MobileDeviceTypes: Set = new Set([ - DeviceType.Android, - DeviceType.iOS, - DeviceType.AndroidAmazon, -]); +/** + * Device type metadata + * Each device type has a category corresponding to the client type and platform (Android, iOS, Chrome, Firefox, etc.) + */ +interface DeviceTypeMetadata { + category: "mobile" | "extension" | "webVault" | "desktop" | "cli" | "sdk" | "server"; + platform: string; +} -export const DesktopDeviceTypes: Set = new Set([ - DeviceType.WindowsDesktop, - DeviceType.MacOsDesktop, - DeviceType.LinuxDesktop, - DeviceType.UWP, - DeviceType.WindowsCLI, - DeviceType.MacOsCLI, - DeviceType.LinuxCLI, -]); +export const DeviceTypeMetadata: Record = { + [DeviceType.Android]: { category: "mobile", platform: "Android" }, + [DeviceType.iOS]: { category: "mobile", platform: "iOS" }, + [DeviceType.AndroidAmazon]: { category: "mobile", platform: "Amazon" }, + [DeviceType.ChromeExtension]: { category: "extension", platform: "Chrome" }, + [DeviceType.FirefoxExtension]: { category: "extension", platform: "Firefox" }, + [DeviceType.OperaExtension]: { category: "extension", platform: "Opera" }, + [DeviceType.EdgeExtension]: { category: "extension", platform: "Edge" }, + [DeviceType.VivaldiExtension]: { category: "extension", platform: "Vivaldi" }, + [DeviceType.SafariExtension]: { category: "extension", platform: "Safari" }, + [DeviceType.ChromeBrowser]: { category: "webVault", platform: "Chrome" }, + [DeviceType.FirefoxBrowser]: { category: "webVault", platform: "Firefox" }, + [DeviceType.OperaBrowser]: { category: "webVault", platform: "Opera" }, + [DeviceType.EdgeBrowser]: { category: "webVault", platform: "Edge" }, + [DeviceType.IEBrowser]: { category: "webVault", platform: "IE" }, + [DeviceType.SafariBrowser]: { category: "webVault", platform: "Safari" }, + [DeviceType.VivaldiBrowser]: { category: "webVault", platform: "Vivaldi" }, + [DeviceType.UnknownBrowser]: { category: "webVault", platform: "Unknown" }, + [DeviceType.WindowsDesktop]: { category: "desktop", platform: "Windows" }, + [DeviceType.MacOsDesktop]: { category: "desktop", platform: "macOS" }, + [DeviceType.LinuxDesktop]: { category: "desktop", platform: "Linux" }, + [DeviceType.UWP]: { category: "desktop", platform: "Windows UWP" }, + [DeviceType.WindowsCLI]: { category: "cli", platform: "Windows" }, + [DeviceType.MacOsCLI]: { category: "cli", platform: "macOS" }, + [DeviceType.LinuxCLI]: { category: "cli", platform: "Linux" }, + [DeviceType.SDK]: { category: "sdk", platform: "" }, + [DeviceType.Server]: { category: "server", platform: "" }, +}; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 0ab7d47acfc..dde31acb9e3 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -4,6 +4,18 @@ * Flags MUST be short lived and SHALL be removed once enabled. */ export enum FeatureFlag { + /* Autofill */ + BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", + DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", + EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", + GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", + InlineMenuFieldQualification = "inline-menu-field-qualification", + InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", + InlineMenuTotp = "inline-menu-totp", + NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", + NotificationRefresh = "notification-refresh", + UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", + BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", GeneratorToolsModernization = "generator-tools-modernization", @@ -11,23 +23,15 @@ export enum FeatureFlag { ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", - UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection", EmailVerification = "email-verification", - InlineMenuFieldQualification = "inline-menu-field-qualification", TwoFactorComponentRefactor = "two-factor-component-refactor", - InlineMenuPositioningImprovements = "inline-menu-positioning-improvements", ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner", VaultBulkManagementAction = "vault-bulk-management-action", IdpAutoSubmitLogin = "idp-auto-submit-login", UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh", - GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor", - EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill", - DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2", AccountDeprovisioning = "pm-10308-account-deprovisioning", SSHKeyVaultItem = "ssh-key-vault-item", SSHAgent = "ssh-agent", - NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", - BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain", AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", @@ -39,7 +43,6 @@ export enum FeatureFlag { NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss", NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss", DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship", - InlineMenuTotp = "inline-menu-totp", MacOsNativeCredentialSync = "macos-native-credential-sync", PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission", PM12443RemovePagingLogic = "pm-12443-remove-paging-logic", @@ -59,6 +62,18 @@ const FALSE = false as boolean; * We support true as a value as we prefer flags to "enable" not "disable". */ export const DefaultFeatureFlagValue = { + /* Autofill */ + [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, + [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, + [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, + [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, + [FeatureFlag.InlineMenuFieldQualification]: FALSE, + [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, + [FeatureFlag.InlineMenuTotp]: FALSE, + [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, + [FeatureFlag.NotificationRefresh]: FALSE, + [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, + [FeatureFlag.BrowserFilelessImport]: FALSE, [FeatureFlag.ItemShare]: FALSE, [FeatureFlag.GeneratorToolsModernization]: FALSE, @@ -66,23 +81,15 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, - [FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE, [FeatureFlag.EmailVerification]: FALSE, - [FeatureFlag.InlineMenuFieldQualification]: FALSE, [FeatureFlag.TwoFactorComponentRefactor]: FALSE, - [FeatureFlag.InlineMenuPositioningImprovements]: FALSE, [FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE, [FeatureFlag.VaultBulkManagementAction]: FALSE, [FeatureFlag.IdpAutoSubmitLogin]: FALSE, [FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE, - [FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE, - [FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE, - [FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE, [FeatureFlag.AccountDeprovisioning]: FALSE, [FeatureFlag.SSHKeyVaultItem]: FALSE, [FeatureFlag.SSHAgent]: FALSE, - [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, - [FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE, [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, @@ -94,7 +101,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE, [FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE, [FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE, - [FeatureFlag.InlineMenuTotp]: FALSE, [FeatureFlag.MacOsNativeCredentialSync]: FALSE, [FeatureFlag.PM11360RemoveProviderExportPermission]: FALSE, [FeatureFlag.PM12443RemovePagingLogic]: FALSE, diff --git a/libs/common/src/enums/notification-type.enum.ts b/libs/common/src/enums/notification-type.enum.ts index 69cbdff9dd2..db59fcafa69 100644 --- a/libs/common/src/enums/notification-type.enum.ts +++ b/libs/common/src/enums/notification-type.enum.ts @@ -23,4 +23,5 @@ export enum NotificationType { SyncOrganizations = 17, SyncOrganizationStatusChanged = 18, + SyncOrganizationCollectionSettingChanged = 19, } diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts index 961d199b06e..9f97e0a94c1 100644 --- a/libs/common/src/key-management/services/default-process-reload.service.ts +++ b/libs/common/src/key-management/services/default-process-reload.service.ts @@ -2,15 +2,18 @@ // @ts-strict-ignore import { firstValueFrom, map, timeout } from "rxjs"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; +import { MessagingService } from "../../platform/abstractions/messaging.service"; import { UserId } from "../../types/guid"; import { ProcessReloadServiceAbstraction } from "../abstractions/process-reload.service"; @@ -24,6 +27,7 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, private accountService: AccountService, + private logService: LogService, ) {} async startProcessReload(authService: AuthService): Promise { @@ -35,6 +39,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); status = await authService.getAuthStatus(userId); if (status === AuthenticationStatus.Unlocked) { + this.logService.info( + "[Process Reload Service] User unlocked, preventing process reload", + ); return; } } @@ -51,6 +58,9 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract if (userId != null) { const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId); if (ephemeralPin != null) { + this.logService.info( + "[Process Reload Service] Ephemeral pin active, preventing process reload", + ); return; } } @@ -93,7 +103,12 @@ export class DefaultProcessReloadService implements ProcessReloadServiceAbstract await this.reloadCallback(); } return; + } else { + this.logService.info( + "[Process Reload Service] Desktop ipc fingerprint validated, preventing process reload", + ); } + if (this.reloadInterval == null) { this.reloadInterval = setInterval(async () => await this.executeProcessReload(), 1000); } diff --git a/libs/common/src/models/export/ssh-key.export.ts b/libs/common/src/models/export/ssh-key.export.ts index c502ddafd47..a99ebac34b3 100644 --- a/libs/common/src/models/export/ssh-key.export.ts +++ b/libs/common/src/models/export/ssh-key.export.ts @@ -1,9 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { SshKeyView as SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { EncString } from "../../platform/models/domain/enc-string"; import { SshKey as SshKeyDomain } from "../../vault/models/domain/ssh-key"; +import { SshKeyView as SshKeyView } from "../../vault/models/view/ssh-key.view"; import { safeGetString } from "./utils"; diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts index 630b4898503..b7e0b74b611 100644 --- a/libs/common/src/models/export/utils.ts +++ b/libs/common/src/models/export/utils.ts @@ -1,4 +1,4 @@ -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { EncString } from "../../platform/models/domain/enc-string"; export function safeGetString(value: string | EncString) { if (value == null) { diff --git a/libs/common/src/models/response/notification.response.ts b/libs/common/src/models/response/notification.response.ts index 473e6fc1d10..894a00ee885 100644 --- a/libs/common/src/models/response/notification.response.ts +++ b/libs/common/src/models/response/notification.response.ts @@ -45,6 +45,9 @@ export class NotificationResponse extends BaseResponse { case NotificationType.SyncOrganizationStatusChanged: this.payload = new OrganizationStatusPushNotification(payload); break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + this.payload = new OrganizationCollectionSettingChangedPushNotification(payload); + break; default: break; } @@ -126,3 +129,17 @@ export class OrganizationStatusPushNotification extends BaseResponse { this.enabled = this.getResponseProperty("Enabled"); } } + +export class OrganizationCollectionSettingChangedPushNotification extends BaseResponse { + organizationId: string; + limitCollectionCreation: boolean; + limitCollectionDeletion: boolean; + + constructor(response: any) { + super(response); + + this.organizationId = this.getResponseProperty("OrganizationId"); + this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation"); + this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion"); + } +} diff --git a/libs/common/src/platform/abstractions/crypto-function.service.ts b/libs/common/src/platform/abstractions/crypto-function.service.ts index 18c14677dd0..56b0ee55afe 100644 --- a/libs/common/src/platform/abstractions/crypto-function.service.ts +++ b/libs/common/src/platform/abstractions/crypto-function.service.ts @@ -1,5 +1,5 @@ import { CsprngArray } from "../../types/csprng"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class CryptoFunctionService { @@ -51,11 +51,13 @@ export abstract class CryptoFunctionService { iv: string, mac: string, key: SymmetricCryptoKey, - ): DecryptParameters; - abstract aesDecryptFast( - parameters: DecryptParameters, - mode: "cbc" | "ecb", - ): Promise; + ): CbcDecryptParameters; + abstract aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise; abstract aesDecrypt( data: Uint8Array, iv: Uint8Array, diff --git a/libs/common/src/platform/abstractions/encrypt.service.ts b/libs/common/src/platform/abstractions/encrypt.service.ts index 5b28b98803b..a660524699d 100644 --- a/libs/common/src/platform/abstractions/encrypt.service.ts +++ b/libs/common/src/platform/abstractions/encrypt.service.ts @@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class EncryptService { abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise; abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an EncString to a string + * @param encString - The EncString to decrypt + * @param key - The key to decrypt the EncString with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted string + */ abstract decryptToUtf8( encString: EncString, key: SymmetricCryptoKey, - decryptContext?: string, + decryptTrace?: string, ): Promise; - abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise; + /** + * Decrypts an Encrypted object to a Uint8Array + * @param encThing - The Encrypted object to decrypt + * @param key - The key to decrypt the Encrypted object with + * @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include + * sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt + * @returns The decrypted Uint8Array + */ + abstract decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptTrace?: string, + ): Promise; abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise; abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey; diff --git a/libs/common/src/platform/enums/key-suffix-options.enum.ts b/libs/common/src/platform/enums/key-suffix-options.enum.ts index b268c4b777f..98fa215be6a 100644 --- a/libs/common/src/platform/enums/key-suffix-options.enum.ts +++ b/libs/common/src/platform/enums/key-suffix-options.enum.ts @@ -1,5 +1,4 @@ export enum KeySuffixOptions { Auto = "auto", - Biometric = "biometric", Pin = "pin", } diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index b52879d88fa..30531b6799e 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -1,18 +1,17 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedFlags = { sdk?: boolean; prereleaseBuild?: boolean; }; // required to avoid linting errors when there are no flags -// eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; skipWelcomeOnInstall: boolean; configRetrievalIntervalMs: number; + showRiskInsightsDebug: boolean; }; function getFlags(envFlags: string | T): T { diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index d2bf11a20ce..f654897e9e2 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -8,10 +8,14 @@ import { Observable, of, switchMap } from "rxjs"; import { getHostname, parse } from "tldts"; import { Merge } from "type-fest"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-require-imports const nodeURL = typeof self === "undefined" ? require("url") : null; declare global { @@ -608,6 +612,8 @@ export class Utils { } return new URL(uriString); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error } diff --git a/libs/common/src/platform/models/domain/decrypt-parameters.ts b/libs/common/src/platform/models/domain/decrypt-parameters.ts index 784826d3bd2..d3b4bf60d42 100644 --- a/libs/common/src/platform/models/domain/decrypt-parameters.ts +++ b/libs/common/src/platform/models/domain/decrypt-parameters.ts @@ -1,10 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore -export class DecryptParameters { +export type CbcDecryptParameters = { encKey: T; data: T; iv: T; - macKey: T; - mac: T; + macKey?: T; + mac?: T; macData: T; -} +}; + +export type EcbDecryptParameters = { + encKey: T; + data: T; +}; diff --git a/libs/common/src/platform/models/domain/domain-base.spec.ts b/libs/common/src/platform/models/domain/domain-base.spec.ts index 0bdee21e3c1..80a4e5e8606 100644 --- a/libs/common/src/platform/models/domain/domain-base.spec.ts +++ b/libs/common/src/platform/models/domain/domain-base.spec.ts @@ -67,9 +67,13 @@ describe("DomainBase", () => { ); // @ts-expect-error -- encString2 was not decrypted + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: string; plainText: string }; // encString2 was not decrypted, so it's still an EncString + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions decrypted as { encToString: string; encString2: EncString; plainText: string }; }); diff --git a/libs/common/src/platform/models/domain/domain-base.ts b/libs/common/src/platform/models/domain/domain-base.ts index bd9139999b7..110a1dc7208 100644 --- a/libs/common/src/platform/models/domain/domain-base.ts +++ b/libs/common/src/platform/models/domain/domain-base.ts @@ -8,7 +8,7 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString } from "./enc-string"; import { SymmetricCryptoKey } from "./symmetric-crypto-key"; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type type EncStringKeys = ConditionalKeys, EncString>; export type DecryptedObject< TEncryptedObject, @@ -63,6 +63,7 @@ export default class Domain { map: any, orgId: string, key: SymmetricCryptoKey = null, + objectContext: string = "No Domain Context", ): Promise { const promises = []; const self: any = this; @@ -78,7 +79,11 @@ export default class Domain { .then(() => { const mapProp = map[theProp] || theProp; if (self[mapProp]) { - return self[mapProp].decrypt(orgId, key); + return self[mapProp].decrypt( + orgId, + key, + `Property: ${prop}; ObjectContext: ${objectContext}`, + ); } return null; }) @@ -114,12 +119,21 @@ export default class Domain { key: SymmetricCryptoKey, encryptService: EncryptService, _: Constructor = this.constructor as Constructor, + objectContext: string = "No Domain Context", ): Promise> { const promises = []; for (const prop of encryptedProperties) { const value = (this as any)[prop] as EncString; - promises.push(this.decryptProperty(prop, value, key, encryptService)); + promises.push( + this.decryptProperty( + prop, + value, + key, + encryptService, + `Property: ${prop.toString()}; ObjectContext: ${objectContext}`, + ), + ); } const decryptedObjects = await Promise.all(promises); @@ -137,10 +151,11 @@ export default class Domain { value: EncString, key: SymmetricCryptoKey, encryptService: EncryptService, + decryptTrace: string, ) { let decrypted: string = null; if (value) { - decrypted = await value.decryptWithKey(key, encryptService); + decrypted = await value.decryptWithKey(key, encryptService, decryptTrace); } else { decrypted = null; } diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts index 85108a9609b..b4916b9f70a 100644 --- a/libs/common/src/platform/models/domain/enc-string.spec.ts +++ b/libs/common/src/platform/models/domain/enc-string.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString, makeStaticByteArray } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; diff --git a/libs/common/src/platform/models/domain/enc-string.ts b/libs/common/src/platform/models/domain/enc-string.ts index c484c80ee5b..f148664a4f9 100644 --- a/libs/common/src/platform/models/domain/enc-string.ts +++ b/libs/common/src/platform/models/domain/enc-string.ts @@ -125,6 +125,8 @@ export class EncString implements Encrypted { try { encType = parseInt(headerPieces[0], null); encPieces = headerPieces[1].split("|"); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return { encType: NaN, encPieces: [] }; } @@ -156,21 +158,21 @@ export class EncString implements Encrypted { return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length; } - async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise { + async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise { if (this.decryptedValue != null) { return this.decryptedValue; } - let keyContext = "provided-key"; + let decryptTrace = "provided-key"; try { if (key == null) { key = await this.getKeyForDecryption(orgId); - keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; + decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey"; if (orgId != null) { - keyContext = `domain-orgkey-${orgId}`; + decryptTrace = `domain-orgkey-${orgId}`; } else { const cryptoService = Utils.getContainerService().getKeyService(); - keyContext = + decryptTrace = (await cryptoService.getUserKey()) == null ? "domain-withlegacysupport-masterkey" : "domain-withlegacysupport-userkey"; @@ -181,20 +183,32 @@ export class EncString implements Encrypted { } const encryptService = Utils.getContainerService().getEncryptService(); - this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext); + this.decryptedValue = await encryptService.decryptToUtf8( + this, + key, + decryptTrace == null ? context : `${decryptTrace}${context || ""}`, + ); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } return this.decryptedValue; } - async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) { + async decryptWithKey( + key: SymmetricCryptoKey, + encryptService: EncryptService, + decryptTrace: string = "domain-withkey", + ): Promise { try { if (key == null) { throw new Error("No key to decrypt EncString"); } - this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey"); + this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.decryptedValue = DECRYPT_ERROR; } diff --git a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts index f467cb8d6e4..eab4c7b2114 100644 --- a/libs/common/src/platform/models/domain/symmetric-crypto-key.ts +++ b/libs/common/src/platform/models/domain/symmetric-crypto-key.ts @@ -7,7 +7,7 @@ import { EncryptionType } from "../../enums"; export class SymmetricCryptoKey { key: Uint8Array; - encKey?: Uint8Array; + encKey: Uint8Array; macKey?: Uint8Array; encType: EncryptionType; @@ -48,12 +48,8 @@ export class SymmetricCryptoKey { throw new Error("Unsupported encType/key length."); } - if (this.key != null) { - this.keyB64 = Utils.fromBufferToB64(this.key); - } - if (this.encKey != null) { - this.encKeyB64 = Utils.fromBufferToB64(this.encKey); - } + this.keyB64 = Utils.fromBufferToB64(this.key); + this.encKeyB64 = Utils.fromBufferToB64(this.encKey); if (this.macKey != null) { this.macKeyB64 = Utils.fromBufferToB64(this.macKey); } diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts index 6022e097ab0..c3e727a2e1e 100644 --- a/libs/common/src/platform/services/container.service.ts +++ b/libs/common/src/platform/services/container.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { EncryptService } from "../abstractions/encrypt.service"; diff --git a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts index 0a85b34eba8..68263cadf27 100644 --- a/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/encrypt.service.implementation.ts @@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService { const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac); if (!macsEqual) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " + + "[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " + encryptionTypeName(key.encType) + "Payload type " + encryptionTypeName(encString.encryptionType) + @@ -125,10 +125,14 @@ export class EncryptServiceImplementation implements EncryptService { } } - return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc"); + return await this.cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters: fastParams }); } - async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise { + async decryptToBytes( + encThing: Encrypted, + key: SymmetricCryptoKey, + decryptContext: string = "no context", + ): Promise { if (key == null) { throw new Error("No encryption key provided."); } @@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService { "[Encrypt service] Key encryption type does not match payload encryption type. Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -167,11 +175,13 @@ export class EncryptServiceImplementation implements EncryptService { const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256"); if (computedMac === null) { this.logMacFailed( - "[Encrypt service] Failed to compute MAC." + + "[Encrypt service#decryptToBytes] Failed to compute MAC." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } @@ -179,11 +189,13 @@ export class EncryptServiceImplementation implements EncryptService { const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac); if (!macsMatch) { this.logMacFailed( - "[Encrypt service] MAC comparison failed. Key or payload has changed." + + "[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." + " Key type " + encryptionTypeName(key.encType) + " Payload type " + - encryptionTypeName(encThing.encryptionType), + encryptionTypeName(encThing.encryptionType) + + " Decrypt context: " + + decryptContext, ); return null; } diff --git a/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts b/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts index ce9426b4db0..8089e081805 100644 --- a/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-active-request-manager.spec.ts @@ -1,14 +1,18 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, Observable } from "rxjs"; -// FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths -import { flushPromises } from "@bitwarden/browser/src/autofill/spec/testing-utils"; - import { Fido2CredentialView } from "../../../vault/models/view/fido2-credential.view"; import { Fido2ActiveRequestManager } from "./fido2-active-request-manager"; +// Duplicated from `apps/browser/src/autofill/spec/testing-utils.ts`. +const scheduler = typeof setImmediate === "function" ? setImmediate : setTimeout; +function flushPromises() { + return new Promise(function (resolve) { + scheduler(resolve); + }); +} + jest.mock("rxjs", () => { const rxjs = jest.requireActual("rxjs"); const { firstValueFrom } = rxjs; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts index 23a8ba3138b..16b3968045a 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -1,5 +1,7 @@ import { mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { DefaultKeyService } from "../../../../key-management/src/key.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts index abb8993c39c..a8947a49f45 100644 --- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -1,3 +1,5 @@ +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { UserId } from "../../types/guid"; import { KeySuffixOptions } from "../enums"; diff --git a/libs/common/src/platform/services/web-crypto-function.service.spec.ts b/libs/common/src/platform/services/web-crypto-function.service.spec.ts index 71f2828855f..1929e6454ef 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.spec.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.spec.ts @@ -2,7 +2,7 @@ import { mock } from "jest-mock-extended"; import { Utils } from "../../platform/misc/utils"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { WebCryptoFunctionService } from "./web-crypto-function.service"; @@ -253,8 +253,13 @@ describe("WebCrypto Function Service", () => { const encData = Utils.fromBufferToB64(encValue); const b64Iv = Utils.fromBufferToB64(iv); const symKey = new SymmetricCryptoKey(key); - const params = cryptoFunctionService.aesDecryptFastParameters(encData, b64Iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters( + encData, + b64Iv, + null, + symKey, + ); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe(value); }); @@ -276,8 +281,8 @@ describe("WebCrypto Function Service", () => { const iv = Utils.fromBufferToB64(makeStaticByteArray(16)); const symKey = new SymmetricCryptoKey(makeStaticByteArray(32)); const data = "ByUF8vhyX4ddU9gcooznwA=="; - const params = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "cbc"); + const parameters = cryptoFunctionService.aesDecryptFastParameters(data, iv, null, symKey); + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "cbc", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -287,10 +292,11 @@ describe("WebCrypto Function Service", () => { const cryptoFunctionService = getWebCryptoFunctionService(); const key = makeStaticByteArray(32); const data = Utils.fromB64ToArray("z5q2XSxYCdQFdI+qK2yLlw=="); - const params = new DecryptParameters(); - params.encKey = Utils.fromBufferToByteString(key); - params.data = Utils.fromBufferToByteString(data); - const decValue = await cryptoFunctionService.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + encKey: Utils.fromBufferToByteString(key), + data: Utils.fromBufferToByteString(data), + }; + const decValue = await cryptoFunctionService.aesDecryptFast({ mode: "ecb", parameters }); expect(decValue).toBe("EncryptMe!"); }); }); @@ -304,6 +310,15 @@ describe("WebCrypto Function Service", () => { const decValue = await cryptoFunctionService.aesDecrypt(data, iv, key, "cbc"); expect(Utils.fromBufferToUtf8(decValue)).toBe("EncryptMe!"); }); + + it("throws if iv is not provided", async () => { + const cryptoFunctionService = getWebCryptoFunctionService(); + const key = makeStaticByteArray(32); + const data = Utils.fromB64ToArray("ByUF8vhyX4ddU9gcooznwA=="); + await expect(() => cryptoFunctionService.aesDecrypt(data, null, key, "cbc")).rejects.toThrow( + "IV is required for CBC mode", + ); + }); }); describe("aesDecrypt ECB mode", () => { diff --git a/libs/common/src/platform/services/web-crypto-function.service.ts b/libs/common/src/platform/services/web-crypto-function.service.ts index c0592654849..61edf7a13b1 100644 --- a/libs/common/src/platform/services/web-crypto-function.service.ts +++ b/libs/common/src/platform/services/web-crypto-function.service.ts @@ -1,12 +1,10 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import * as argon2 from "argon2-browser"; import * as forge from "node-forge"; import { Utils } from "../../platform/misc/utils"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { DecryptParameters } from "../models/domain/decrypt-parameters"; +import { CbcDecryptParameters, EcbDecryptParameters } from "../models/domain/decrypt-parameters"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export class WebCryptoFunctionService implements CryptoFunctionService { @@ -14,10 +12,14 @@ export class WebCryptoFunctionService implements CryptoFunctionService { private subtle: SubtleCrypto; private wasmSupported: boolean; - constructor(globalContext: Window | typeof global) { - this.crypto = typeof globalContext.crypto !== "undefined" ? globalContext.crypto : null; - this.subtle = - !!this.crypto && typeof this.crypto.subtle !== "undefined" ? this.crypto.subtle : null; + constructor(globalContext: { crypto: Crypto }) { + if (globalContext?.crypto?.subtle == null) { + throw new Error( + "Could not instantiate WebCryptoFunctionService. Could not locate Subtle crypto.", + ); + } + this.crypto = globalContext.crypto; + this.subtle = this.crypto.subtle; this.wasmSupported = this.checkIfWasmSupported(); } @@ -220,7 +222,7 @@ export class WebCryptoFunctionService implements CryptoFunctionService { hmac.update(a); const mac1 = hmac.digest().getBytes(); - hmac.start(null, null); + hmac.start("sha256", null); hmac.update(b); const mac2 = hmac.digest().getBytes(); @@ -239,10 +241,10 @@ export class WebCryptoFunctionService implements CryptoFunctionService { aesDecryptFastParameters( data: string, iv: string, - mac: string, + mac: string | null, key: SymmetricCryptoKey, - ): DecryptParameters { - const p = new DecryptParameters(); + ): CbcDecryptParameters { + const p = {} as CbcDecryptParameters; if (key.meta != null) { p.encKey = key.meta.encKeyByteString; p.macKey = key.meta.macKeyByteString; @@ -275,7 +277,12 @@ export class WebCryptoFunctionService implements CryptoFunctionService { return p; } - aesDecryptFast(parameters: DecryptParameters, mode: "cbc" | "ecb"): Promise { + aesDecryptFast({ + mode, + parameters, + }: + | { mode: "cbc"; parameters: CbcDecryptParameters } + | { mode: "ecb"; parameters: EcbDecryptParameters }): Promise { const decipher = (forge as any).cipher.createDecipher( this.toWebCryptoAesMode(mode), parameters.encKey, @@ -294,21 +301,27 @@ export class WebCryptoFunctionService implements CryptoFunctionService { async aesDecrypt( data: Uint8Array, - iv: Uint8Array, + iv: Uint8Array | null, key: Uint8Array, mode: "cbc" | "ecb", ): Promise { if (mode === "ecb") { // Web crypto does not support AES-ECB mode, so we need to do this in forge. - const params = new DecryptParameters(); - params.data = this.toByteString(data); - params.encKey = this.toByteString(key); - const result = await this.aesDecryptFast(params, "ecb"); + const parameters: EcbDecryptParameters = { + data: this.toByteString(data), + encKey: this.toByteString(key), + }; + const result = await this.aesDecryptFast({ mode: "ecb", parameters }); return Utils.fromByteStringToArray(result); } const impKey = await this.subtle.importKey("raw", key, { name: "AES-CBC" } as any, false, [ "decrypt", ]); + + // CBC + if (iv == null) { + throw new Error("IV is required for CBC mode."); + } const buffer = await this.subtle.decrypt({ name: "AES-CBC", iv: iv }, impKey, data); return new Uint8Array(buffer); } diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts index 9e36aa69417..138c7c03318 100644 --- a/libs/common/src/platform/sync/default-sync.service.ts +++ b/libs/common/src/platform/sync/default-sync.service.ts @@ -8,8 +8,14 @@ import { CollectionDetailsResponse, } from "@bitwarden/admin-console/common"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/common/abstractions"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { LogoutReason } from "../../../../auth/src/common/types"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction"; diff --git a/libs/common/src/services/notifications.service.ts b/libs/common/src/services/notifications.service.ts index 4a14332af8a..f88c904bee1 100644 --- a/libs/common/src/services/notifications.service.ts +++ b/libs/common/src/services/notifications.service.ts @@ -227,6 +227,11 @@ export class NotificationsService implements NotificationsServiceAbstraction { await this.syncService.fullSync(true); } break; + case NotificationType.SyncOrganizationCollectionSettingChanged: + if (isAuthenticated) { + await this.syncService.fullSync(true); + } + break; default: break; } diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 540f26bba2d..77ed6c960ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -6,10 +6,10 @@ import { FakeUserDecryptionOptions as UserDecryptionOptions, UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -18,10 +18,12 @@ import { Policy } from "../../admin-console/models/domain/policy"; import { TokenService } from "../../auth/abstractions/token.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { LogService } from "../../platform/abstractions/log.service"; +import { Utils } from "../../platform/misc/utils"; import { VAULT_TIMEOUT, VAULT_TIMEOUT_ACTION, } from "../../services/vault-timeout/vault-timeout-settings.state"; +import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index 0f5bdca93da..ffc8b6e0144 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -21,6 +21,8 @@ import { } from "@bitwarden/auth/common"; import { BiometricStateService } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts index 1350010f849..986bbbf95a4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.spec.ts @@ -3,8 +3,7 @@ import { BehaviorSubject, from, of } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling"; +import { BiometricsService } from "@bitwarden/key-management"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; @@ -14,10 +13,12 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; import { Utils } from "../../platform/misc/utils"; +import { TaskSchedulerService } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "../../types/vault-timeout.type"; @@ -41,6 +42,7 @@ describe("VaultTimeoutService", () => { let stateEventRunnerService: MockProxy; let taskSchedulerService: MockProxy; let logService: MockProxy; + let biometricsService: MockProxy; let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [logoutReason: LogoutReason, userId?: string]>; @@ -66,6 +68,7 @@ describe("VaultTimeoutService", () => { stateEventRunnerService = mock(); taskSchedulerService = mock(); logService = mock(); + biometricsService = mock(); lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); @@ -93,6 +96,7 @@ describe("VaultTimeoutService", () => { stateEventRunnerService, taskSchedulerService, logService, + biometricsService, lockedCallback, loggedOutCallback, ); diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 55d5bffa99a..08dc02bb1ab 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -4,8 +4,7 @@ import { combineLatest, concatMap, filter, firstValueFrom, map, timeout } from " import { CollectionService } from "@bitwarden/admin-console/common"; import { LogoutReason } from "@bitwarden/auth/common"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { TaskSchedulerService, ScheduledTaskNames } from "@bitwarden/common/platform/scheduling"; +import { BiometricsService } from "@bitwarden/key-management"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -15,9 +14,11 @@ import { AuthService } from "../../auth/abstractions/auth.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; +import { TaskSchedulerService, ScheduledTaskNames } from "../../platform/scheduling"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; @@ -41,6 +42,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private stateEventRunnerService: StateEventRunnerService, private taskSchedulerService: TaskSchedulerService, protected logService: LogService, + private biometricService: BiometricsService, private lockedCallback: (userId?: string) => Promise = null, private loggedOutCallback: ( logoutReason: LogoutReason, @@ -98,6 +100,8 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { } async lock(userId?: UserId): Promise { + await this.biometricService.setShouldAutopromptNow(false); + const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; diff --git a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts index 53ae94c30b2..84bcaeec608 100644 --- a/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/18-move-autofill-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const AutofillOverlayVisibility = { Off: 0, OnButtonClick: 1, diff --git a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts index 04b4a50e0ed..9397191f54b 100644 --- a/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/25-move-clear-clipboard-to-autofill-settings-state-provider.ts @@ -3,6 +3,8 @@ import { StateDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const ClearClipboardDelay = { Never: null as null, TenSeconds: 10, diff --git a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts index b465a69131a..71e5c531d50 100644 --- a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts @@ -3,6 +3,8 @@ import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; import { Migrator } from "../migrator"; +// FIXME: Remove when updating file. Eslint update +// eslint-disable-next-line @typescript-eslint/no-unused-vars const UriMatchStrategy = { Domain: 0, Host: 1, diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts index 12257905d1c..831cad74155 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.spec.ts @@ -1,13 +1,13 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { CsprngArray } from "@bitwarden/common/types/csprng"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "../../types/csprng"; +import { OrganizationId, UserId } from "../../types/guid"; +import { OrgKey, UserKey } from "../../types/key"; import { OrganizationBound, UserBound } from "../dependencies"; import { KeyServiceLegacyEncryptorProvider } from "./key-service-legacy-encryptor-provider"; diff --git a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts index 3eee08eb6bd..d4a8dec7dc3 100644 --- a/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts +++ b/libs/common/src/tools/cryptography/key-service-legacy-encryptor-provider.ts @@ -12,10 +12,10 @@ import { takeWhile, } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { KeyService } from "@bitwarden/key-management"; +import { EncryptService } from "../../platform/abstractions/encrypt.service"; +import { OrganizationId, UserId } from "../../types/guid"; import { OrganizationBound, SingleOrganizationDependency, diff --git a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts index dda8bfe957a..9eec207e92d 100644 --- a/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/organization-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * organization-specific keys. This strategy is bound to a specific organization. diff --git a/libs/common/src/tools/cryptography/organization-key-encryptor.ts b/libs/common/src/tools/cryptography/organization-key-encryptor.ts index 3fdc0a1da32..d3b7dae10f5 100644 --- a/libs/common/src/tools/cryptography/organization-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/organization-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { OrganizationId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { OrganizationId } from "../../types/guid"; import { OrgKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts index d63a5e908ef..6bb0e252af3 100644 --- a/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts +++ b/libs/common/src/tools/cryptography/user-encryptor.abstraction.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; /** An encryption strategy that protects a type's secrets with * user-specific keys. This strategy is bound to a specific user. diff --git a/libs/common/src/tools/cryptography/user-key-encryptor.ts b/libs/common/src/tools/cryptography/user-key-encryptor.ts index 15a3a1b6d5c..296c33ea1dc 100644 --- a/libs/common/src/tools/cryptography/user-key-encryptor.ts +++ b/libs/common/src/tools/cryptography/user-key-encryptor.ts @@ -2,10 +2,9 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncString } from "../../platform/models/domain/enc-string"; +import { UserId } from "../../types/guid"; import { UserKey } from "../../types/key"; import { DataPacker } from "../state/data-packer.abstraction"; diff --git a/libs/common/src/tools/dependencies.ts b/libs/common/src/tools/dependencies.ts index cdae45bc94a..c22e71cff67 100644 --- a/libs/common/src/tools/dependencies.ts +++ b/libs/common/src/tools/dependencies.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; -import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { Policy } from "../admin-console/models/domain/policy"; +import { OrganizationId, UserId } from "../types/guid"; import { OrganizationEncryptor } from "./cryptography/organization-encryptor.abstraction"; import { UserEncryptor } from "./cryptography/user-encryptor.abstraction"; diff --git a/libs/common/src/tools/integration/integration-context.spec.ts b/libs/common/src/tools/integration/integration-context.spec.ts index 58115c783c7..42581c08dee 100644 --- a/libs/common/src/tools/integration/integration-context.spec.ts +++ b/libs/common/src/tools/integration/integration-context.spec.ts @@ -1,6 +1,6 @@ import { mock } from "jest-mock-extended"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { I18nService } from "../../platform/abstractions/i18n.service"; import { IntegrationContext } from "./integration-context"; import { IntegrationId } from "./integration-id"; diff --git a/libs/common/src/tools/integration/integration-context.ts b/libs/common/src/tools/integration/integration-context.ts index f30810ff81a..40648df6803 100644 --- a/libs/common/src/tools/integration/integration-context.ts +++ b/libs/common/src/tools/integration/integration-context.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { I18nService } from "../../platform/abstractions/i18n.service"; +import { Utils } from "../../platform/misc/utils"; import { IntegrationMetadata } from "./integration-metadata"; import { ApiSettings, IntegrationRequest } from "./rpc"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.spec.ts b/libs/common/src/tools/integration/rpc/rest-client.spec.ts index e113ab9ff43..9b76d305e6f 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.spec.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.spec.ts @@ -1,7 +1,7 @@ import { mock } from "jest-mock-extended"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { RestClient } from "./rest-client"; diff --git a/libs/common/src/tools/integration/rpc/rest-client.ts b/libs/common/src/tools/integration/rpc/rest-client.ts index 287bb4f2573..c42244166e7 100644 --- a/libs/common/src/tools/integration/rpc/rest-client.ts +++ b/libs/common/src/tools/integration/rpc/rest-client.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ApiService } from "../../../abstractions/api.service"; +import { I18nService } from "../../../platform/abstractions/i18n.service"; import { IntegrationRequest } from "./integration-request"; import { JsonRpc } from "./rpc"; diff --git a/libs/common/src/tools/private-classifier.ts b/libs/common/src/tools/private-classifier.ts index de21f78f1c6..58244ae9906 100644 --- a/libs/common/src/tools/private-classifier.ts +++ b/libs/common/src/tools/private-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PrivateClassifier implements Classifier, Data> { constructor(private keys: (keyof Jsonify)[] = undefined) {} diff --git a/libs/common/src/tools/public-classifier.ts b/libs/common/src/tools/public-classifier.ts index e7c8c24ba78..e036ebd1c42 100644 --- a/libs/common/src/tools/public-classifier.ts +++ b/libs/common/src/tools/public-classifier.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { Classifier } from "@bitwarden/common/tools/state/classifier"; +import { Classifier } from "./state/classifier"; export class PublicClassifier implements Classifier> { constructor(private keys: (keyof Jsonify)[]) {} diff --git a/libs/common/src/tools/send/models/domain/send.spec.ts b/libs/common/src/tools/send/models/domain/send.spec.ts index 74c0e77b394..fcc273d41bb 100644 --- a/libs/common/src/tools/send/models/domain/send.spec.ts +++ b/libs/common/src/tools/send/models/domain/send.spec.ts @@ -1,12 +1,12 @@ import { mock } from "jest-mock-extended"; -import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { UserKey } from "@bitwarden/common/types/key"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc } from "../../../../../spec"; import { EncryptService } from "../../../../platform/abstractions/encrypt.service"; +import { SymmetricCryptoKey } from "../../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../../platform/services/container.service"; +import { UserKey } from "../../../../types/key"; import { SendType } from "../../enums/send-type"; import { SendData } from "../data/send.data"; @@ -123,7 +123,12 @@ describe("Send", () => { const view = await send.decrypt(); expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey"); - expect(send.name.decrypt).toHaveBeenNthCalledWith(1, null, "cryptoKey"); + expect(send.name.decrypt).toHaveBeenNthCalledWith( + 1, + null, + "cryptoKey", + "Property: name; ObjectContext: No Domain Context", + ); expect(view).toMatchObject({ id: "id", diff --git a/libs/common/src/tools/send/models/domain/send.ts b/libs/common/src/tools/send/models/domain/send.ts index 43115b65937..c2390d439e7 100644 --- a/libs/common/src/tools/send/models/domain/send.ts +++ b/libs/common/src/tools/send/models/domain/send.ts @@ -81,6 +81,8 @@ export class Send extends Domain { const sendKeyEncryptionKey = await keyService.getUserKey(); model.key = await encryptService.decryptToBytes(this.key, sendKeyEncryptionKey); model.cryptoKey = await keyService.makeSendKey(model.key); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index a6427824a64..570f3e746a0 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -22,11 +22,6 @@ export abstract class SendApiService { postSend: (request: SendRequest) => Promise; postFileTypeSend: (request: SendRequest) => Promise; postSendFile: (sendId: string, fileId: string, data: FormData) => Promise; - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - postSendFileLegacy: (data: FormData) => Promise; putSend: (id: string, request: SendRequest) => Promise; putSendRemovePassword: (id: string) => Promise; deleteSend: (id: string) => Promise; diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index ff71408bce3..f709553646f 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -5,7 +5,6 @@ import { FileUploadApiMethods, FileUploadService, } from "../../../platform/abstractions/file-upload/file-upload.service"; -import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { SendType } from "../enums/send-type"; import { SendData } from "../models/data/send.data"; @@ -106,15 +105,6 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("POST", "/sends/" + sendId + "/file/" + fileId, data, true, false); } - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async postSendFileLegacy(data: FormData): Promise { - const r = await this.apiService.send("POST", "/sends/file", data, true, true); - return new SendResponse(r); - } - async putSend(id: string, request: SendRequest): Promise { const r = await this.apiService.send("PUT", "/sends/" + id, request, true, true); return new SendResponse(r); @@ -173,9 +163,7 @@ export class SendApiService implements SendApiServiceAbstraction { this.generateMethods(uploadDataResponse, response), ); } catch (e) { - if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { - response = await this.legacyServerSendFileUpload(sendData, request); - } else if (e instanceof ErrorResponse) { + if (e instanceof ErrorResponse) { throw new Error((e as ErrorResponse).getSingleMessage()); } else { throw e; @@ -219,35 +207,4 @@ export class SendApiService implements SendApiServiceAbstraction { return this.deleteSend(sendId); }; } - - /** - * @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads. - * This method still exists for backward compatibility with old server versions. - */ - async legacyServerSendFileUpload( - sendData: [Send, EncArrayBuffer], - request: SendRequest, - ): Promise { - const fd = new FormData(); - try { - const blob = new Blob([sendData[1].buffer], { type: "application/octet-stream" }); - fd.append("model", JSON.stringify(request)); - fd.append("data", blob, sendData[0].file.fileName.encryptedString); - } catch (e) { - if (Utils.isNode && !Utils.isBrowser) { - fd.append("model", JSON.stringify(request)); - fd.append( - "data", - Buffer.from(sendData[1].buffer) as any, - { - filepath: sendData[0].file.fileName.encryptedString, - contentType: "application/octet-stream", - } as any, - ); - } else { - throw e; - } - } - return await this.postSendFileLegacy(fd); - } } diff --git a/libs/common/src/tools/send/services/send.service.spec.ts b/libs/common/src/tools/send/services/send.service.spec.ts index 5aca3a4b5c9..662fee02bbf 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -1,10 +1,8 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; -import { SelfHostedEnvironment } from "@bitwarden/common/platform/services/default-environment.service"; +import { KeyService } from "@bitwarden/key-management"; -import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { FakeAccountService, FakeActiveUserState, @@ -13,12 +11,14 @@ import { mockAccountServiceWith, } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; +import { EnvironmentService } from "../../../platform/abstractions/environment.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; import { Utils } from "../../../platform/misc/utils"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { ContainerService } from "../../../platform/services/container.service"; +import { SelfHostedEnvironment } from "../../../platform/services/default-environment.service"; import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { SendType } from "../enums/send-type"; diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index 6a50a1dd668..8111f6f9f17 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -1,9 +1,8 @@ import { BehaviorSubject, of, Subject } from "rxjs"; -import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - import { awaitAsync, FakeSingleUserState, ObservableTracker } from "../../../spec"; +import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { PrivateClassifier } from "../private-classifier"; diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index d508cf71663..c00560304e4 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -26,10 +26,9 @@ import { skip, } from "rxjs"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { SingleUserState, UserKeyDefinition } from "@bitwarden/common/platform/state"; -import { UserId } from "@bitwarden/common/types/guid"; - +import { EncString } from "../../platform/models/domain/enc-string"; +import { SingleUserState, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; import { UserBound } from "../dependencies"; import { anyComplete, errorOnChange, ready, withLatestReady } from "../rx"; diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 870cb8b3d73..88cd476606d 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -2,7 +2,6 @@ // @ts-strict-ignore import { Observable } from "rxjs"; -import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; import { UserKeyRotationDataProvider } from "@bitwarden/key-management"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; @@ -11,6 +10,7 @@ import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid import { UserKey } from "../../types/key"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; +import { LocalData } from "../models/data/local.data"; import { Cipher } from "../models/domain/cipher"; import { Field } from "../models/domain/field"; import { CipherWithIdRequest } from "../models/request/cipher-with-id.request"; @@ -26,6 +26,12 @@ export abstract class CipherService implements UserKeyRotationDataProvider; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; clearCache: (userId?: string) => Promise; encrypt: ( model: CipherView, diff --git a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts index 859f2183edb..b95dd35e5ec 100644 --- a/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts +++ b/libs/common/src/vault/abstractions/folder/folder-api.service.abstraction.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { UserId } from "@bitwarden/common/types/guid"; +import { UserId } from "../../../types/guid"; import { Folder } from "../../models/domain/folder"; import { FolderResponse } from "../../models/response/folder.response"; diff --git a/libs/common/src/vault/icon/build-cipher-icon.ts b/libs/common/src/vault/icon/build-cipher-icon.ts index 78e6ecd7b4f..5775bc7f55e 100644 --- a/libs/common/src/vault/icon/build-cipher-icon.ts +++ b/libs/common/src/vault/icon/build-cipher-icon.ts @@ -43,10 +43,18 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1; } + if (isWebsite && (hostnameUri.endsWith(".onion") || hostnameUri.endsWith(".i2p"))) { + image = null; + fallbackImage = "images/bwi-globe.png"; + break; + } + if (showFavicon && isWebsite) { try { image = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`; fallbackImage = "images/bwi-globe.png"; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Ignore error since the fallback icon will be shown if image is null. } diff --git a/libs/common/src/vault/models/domain/attachment.spec.ts b/libs/common/src/vault/models/domain/attachment.spec.ts index 14dec8dea0c..8cae7170738 100644 --- a/libs/common/src/vault/models/domain/attachment.spec.ts +++ b/libs/common/src/vault/models/domain/attachment.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; @@ -101,7 +103,7 @@ describe("Attachment", () => { it("uses the provided key without depending on KeyService", async () => { const providedKey = mock(); - await attachment.decrypt(null, providedKey); + await attachment.decrypt(null, "", providedKey); expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey); @@ -111,7 +113,7 @@ describe("Attachment", () => { const orgKey = mock(); keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey); - await attachment.decrypt("orgId", null); + await attachment.decrypt("orgId", "", null); expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId"); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey); @@ -121,7 +123,7 @@ describe("Attachment", () => { const userKey = mock(); keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey); - await attachment.decrypt(null, null); + await attachment.decrypt(null, "", null); expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled(); expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey); diff --git a/libs/common/src/vault/models/domain/attachment.ts b/libs/common/src/vault/models/domain/attachment.ts index 1178f441c5e..4eee0307746 100644 --- a/libs/common/src/vault/models/domain/attachment.ts +++ b/libs/common/src/vault/models/domain/attachment.ts @@ -38,7 +38,11 @@ export class Attachment extends Domain { ); } - async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { const view = await this.decryptObj( new AttachmentView(this), { @@ -46,6 +50,7 @@ export class Attachment extends Domain { }, orgId, encKey, + "DomainType: Attachment; " + context, ); if (this.key != null) { @@ -64,6 +69,8 @@ export class Attachment extends Domain { const encryptService = Utils.getContainerService().getEncryptService(); const decValue = await encryptService.decryptToBytes(this.key, encKey); return new SymmetricCryptoKey(decValue); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // TODO: error? } diff --git a/libs/common/src/vault/models/domain/card.ts b/libs/common/src/vault/models/domain/card.ts index 739cbf78465..fccfe3f595b 100644 --- a/libs/common/src/vault/models/domain/card.ts +++ b/libs/common/src/vault/models/domain/card.ts @@ -37,7 +37,11 @@ export class Card extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new CardView(), { @@ -50,6 +54,7 @@ export class Card extends Domain { }, orgId, encKey, + "DomainType: Card; " + context, ); } diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 509a17a8a0e..dd79da3086e 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; -import { UserId } from "@bitwarden/common/types/guid"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { UriMatchStrategy } from "../../../models/domain/domain-service"; @@ -10,6 +10,7 @@ import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../abstractions/cipher.service"; import { FieldType, SecureNoteType } from "../../enums"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; diff --git a/libs/common/src/vault/models/domain/cipher.ts b/libs/common/src/vault/models/domain/cipher.ts index d7355ce5202..d82f4585e65 100644 --- a/libs/common/src/vault/models/domain/cipher.ts +++ b/libs/common/src/vault/models/domain/cipher.ts @@ -136,7 +136,17 @@ export class Cipher extends Domain implements Decryptable { if (this.key != null) { const encryptService = Utils.getContainerService().getEncryptService(); - encKey = new SymmetricCryptoKey(await encryptService.decryptToBytes(this.key, encKey)); + const keyBytes = await encryptService.decryptToBytes( + this.key, + encKey, + `Cipher Id: ${this.id}; Content: CipherKey; IsEncryptedByOrgKey: ${this.organizationId != null}`, + ); + if (keyBytes == null) { + model.name = "[error: cannot decrypt]"; + model.decryptionFailure = true; + return model; + } + encKey = new SymmetricCryptoKey(keyBytes); bypassValidation = false; } @@ -152,19 +162,36 @@ export class Cipher extends Domain implements Decryptable { switch (this.type) { case CipherType.Login: - model.login = await this.login.decrypt(this.organizationId, bypassValidation, encKey); + model.login = await this.login.decrypt( + this.organizationId, + bypassValidation, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SecureNote: - model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey); + model.secureNote = await this.secureNote.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.Card: - model.card = await this.card.decrypt(this.organizationId, encKey); + model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); break; case CipherType.Identity: - model.identity = await this.identity.decrypt(this.organizationId, encKey); + model.identity = await this.identity.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; case CipherType.SshKey: - model.sshKey = await this.sshKey.decrypt(this.organizationId, encKey); + model.sshKey = await this.sshKey.decrypt( + this.organizationId, + `Cipher Id: ${this.id}`, + encKey, + ); break; default: break; @@ -175,7 +202,7 @@ export class Cipher extends Domain implements Decryptable { await this.attachments.reduce((promise, attachment) => { return promise .then(() => { - return attachment.decrypt(this.organizationId, encKey); + return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey); }) .then((decAttachment) => { attachments.push(decAttachment); diff --git a/libs/common/src/vault/models/domain/identity.ts b/libs/common/src/vault/models/domain/identity.ts index e2b7aef52f0..570e6c0b4d5 100644 --- a/libs/common/src/vault/models/domain/identity.ts +++ b/libs/common/src/vault/models/domain/identity.ts @@ -61,7 +61,11 @@ export class Identity extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new IdentityView(), { @@ -86,6 +90,7 @@ export class Identity extends Domain { }, orgId, encKey, + "DomainType: Identity; " + context, ); } diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index 0d7380e034d..36782a81502 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -33,7 +33,11 @@ export class LoginUri extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context: string = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new LoginUriView(this), { @@ -41,6 +45,7 @@ export class LoginUri extends Domain { }, orgId, encKey, + context, ); } diff --git a/libs/common/src/vault/models/domain/login.ts b/libs/common/src/vault/models/domain/login.ts index a0a61a9b857..f9a85cd818e 100644 --- a/libs/common/src/vault/models/domain/login.ts +++ b/libs/common/src/vault/models/domain/login.ts @@ -55,6 +55,7 @@ export class Login extends Domain { async decrypt( orgId: string, bypassValidation: boolean, + context: string = "No Cipher Context", encKey?: SymmetricCryptoKey, ): Promise { const view = await this.decryptObj( @@ -66,6 +67,7 @@ export class Login extends Domain { }, orgId, encKey, + `DomainType: Login; ${context}`, ); if (this.uris != null) { @@ -76,7 +78,7 @@ export class Login extends Domain { continue; } - const uri = await this.uris[i].decrypt(orgId, encKey); + const uri = await this.uris[i].decrypt(orgId, context, encKey); // URIs are shared remotely after decryption // we need to validate that the string hasn't been changed by a compromised server // This validation is tied to the existence of cypher.key for backwards compatibility diff --git a/libs/common/src/vault/models/domain/password.ts b/libs/common/src/vault/models/domain/password.ts index 4c4f465654e..48063f495f0 100644 --- a/libs/common/src/vault/models/domain/password.ts +++ b/libs/common/src/vault/models/domain/password.ts @@ -32,6 +32,7 @@ export class Password extends Domain { }, orgId, encKey, + "DomainType: PasswordHistory", ); } diff --git a/libs/common/src/vault/models/domain/secure-note.ts b/libs/common/src/vault/models/domain/secure-note.ts index 4769ad062d9..693ae38d9fb 100644 --- a/libs/common/src/vault/models/domain/secure-note.ts +++ b/libs/common/src/vault/models/domain/secure-note.ts @@ -20,8 +20,12 @@ export class SecureNote extends Domain { this.type = obj.type; } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { - return Promise.resolve(new SecureNoteView(this)); + async decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { + return new SecureNoteView(this); } toSecureNoteData(): SecureNoteData { diff --git a/libs/common/src/vault/models/domain/ssh-key.ts b/libs/common/src/vault/models/domain/ssh-key.ts index 3a79c1f0022..b4df172e543 100644 --- a/libs/common/src/vault/models/domain/ssh-key.ts +++ b/libs/common/src/vault/models/domain/ssh-key.ts @@ -2,9 +2,8 @@ // @ts-strict-ignore import { Jsonify } from "type-fest"; -import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; - import Domain from "../../../platform/models/domain/domain-base"; +import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SshKeyData } from "../data/ssh-key.data"; import { SshKeyView } from "../view/ssh-key.view"; @@ -32,7 +31,11 @@ export class SshKey extends Domain { ); } - decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise { + decrypt( + orgId: string, + context = "No Cipher Context", + encKey?: SymmetricCryptoKey, + ): Promise { return this.decryptObj( new SshKeyView(), { @@ -42,6 +45,7 @@ export class SshKey extends Domain { }, orgId, encKey, + "DomainType: SshKey; " + context, ); } diff --git a/libs/common/src/vault/models/view/cipher.view.ts b/libs/common/src/vault/models/view/cipher.view.ts index 6416ab31970..20dbd23065c 100644 --- a/libs/common/src/vault/models/view/cipher.view.ts +++ b/libs/common/src/vault/models/view/cipher.view.ts @@ -46,6 +46,11 @@ export class CipherView implements View, InitializerMetadata { deletedDate: Date = null; reprompt: CipherRepromptType = CipherRepromptType.None; + /** + * Flag to indicate if the cipher decryption failed. + */ + decryptionFailure = false; + constructor(c?: Cipher) { if (!c) { return; @@ -150,6 +155,8 @@ export class CipherView implements View, InitializerMetadata { return null; } + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars const item = this.item; return this.item[linkedFieldOption.propertyKey as keyof typeof item]; } diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index a6b7a0c0a22..315adb87c75 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -142,6 +142,8 @@ export class LoginUriView implements View { try { const regex = new RegExp(this.uri, "i"); return regex.test(targetUri); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // Invalid regex return false; diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index cccd29ad697..15e8b03bfad 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -2,10 +2,10 @@ import { mock } from "jest-mock-extended"; import { firstValueFrom, of } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "../../admin-console/models/domain/organization"; +import { CollectionId } from "../../types/guid"; import { CipherView } from "../models/view/cipher.view"; import { diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index 260d1eeece7..025d6b1cdc3 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -3,9 +3,9 @@ import { map, Observable, of, shareReplay, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; -import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { OrganizationService } from "../../admin-console/abstractions/organization/organization.service.abstraction"; +import { CollectionId } from "../../types/guid"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 6b225af0d84..0d6578f165d 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1,8 +1,8 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, map, of } from "rxjs"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { CipherDecryptionKeys, KeyService, @@ -15,6 +15,7 @@ import { SearchService } from "../../abstractions/search.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; import { UriMatchStrategy } from "../../models/domain/domain-service"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -359,6 +360,7 @@ describe("Cipher Service", () => { const originalUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; const newUserKey = new SymmetricCryptoKey(new Uint8Array(32)) as UserKey; let decryptedCiphers: BehaviorSubject>; + let failedCiphers: BehaviorSubject; let encryptedKey: EncString; beforeEach(() => { @@ -385,6 +387,7 @@ describe("Cipher Service", () => { Cipher2: cipher2, }); cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))); + cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject([]); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Cipher Key"); @@ -413,5 +416,16 @@ describe("Cipher Service", () => { "New user key is required to rotate ciphers", ); }); + + it("throws if the user has any failed to decrypt ciphers", async () => { + const badCipher = new CipherView(cipherObj); + badCipher.id = "Cipher 3"; + badCipher.organizationId = null; + badCipher.decryptionFailure = true; + failedCiphers.next([badCipher]); + await expect( + cipherService.getRotatedData(originalUserKey, newUserKey, mockUserId), + ).rejects.toThrow("Cannot rotate ciphers when decryption failures are present"); + }); }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 8711496b374..fe946fbb064 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -7,25 +7,27 @@ import { map, merge, Observable, + of, shareReplay, Subject, switchMap, } from "rxjs"; import { SemVer } from "semver"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { BulkEncryptService } from "@bitwarden/common/platform/abstractions/bulk-encrypt.service"; - +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../key-management/src/abstractions/key.service"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; +import { AccountService } from "../../auth/abstractions/account.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { DomainSettingsService } from "../../autofill/services/domain-settings.service"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { ErrorResponse } from "../../models/response/error.response"; import { ListResponse } from "../../models/response/list.response"; import { View } from "../../models/view/view"; +import { BulkEncryptService } from "../../platform/abstractions/bulk-encrypt.service"; import { ConfigService } from "../../platform/abstractions/config/config.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; @@ -79,6 +81,7 @@ import { ADD_EDIT_CIPHER_INFO_KEY, DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS, + FAILED_DECRYPTED_CIPHERS, LOCAL_DATA_KEY, } from "./key-state/ciphers.state"; @@ -109,9 +112,17 @@ export class CipherService implements CipherServiceAbstraction { cipherViews$: Observable; addEditCipherInfo$: Observable; + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$: Observable; + private localDataState: ActiveUserState>; private encryptedCiphersState: ActiveUserState>; private decryptedCiphersState: ActiveUserState>; + private failedToDecryptCiphersState: ActiveUserState; private addEditCipherInfoState: ActiveUserState; constructor( @@ -132,6 +143,7 @@ export class CipherService implements CipherServiceAbstraction { this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); + this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); @@ -143,6 +155,13 @@ export class CipherService implements CipherServiceAbstraction { switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), shareReplay({ bufferSize: 1, refCount: true }), ); + + this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe( + filter((ciphers) => ciphers != null), + switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), + shareReplay({ bufferSize: 1, refCount: true }), + ); + this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -162,6 +181,10 @@ export class CipherService implements CipherServiceAbstraction { } } + async setFailedDecryptedCiphers(cipherViews: CipherView[], userId: UserId) { + await this.stateProvider.setUserState(FAILED_DECRYPTED_CIPHERS, cipherViews, userId); + } + private async setDecryptedCiphers(value: CipherView[], userId: UserId) { const cipherViews: { [id: string]: CipherView } = {}; value?.forEach((c) => { @@ -378,7 +401,7 @@ export class CipherService implements CipherServiceAbstraction { */ @sequentialize(() => "getAllDecrypted") async getAllDecrypted(): Promise { - let decCiphers = await this.getDecryptedCiphers(); + const decCiphers = await this.getDecryptedCiphers(); if (decCiphers != null && decCiphers.length !== 0) { await this.reindexCiphers(); return await this.getDecryptedCiphers(); @@ -390,10 +413,15 @@ export class CipherService implements CipherServiceAbstraction { return []; } - decCiphers = await this.decryptCiphers(await this.getAll(), activeUserId); + const [newDecCiphers, failedCiphers] = await this.decryptCiphers( + await this.getAll(), + activeUserId, + ); - await this.setDecryptedCipherCache(decCiphers, activeUserId); - return decCiphers; + await this.setDecryptedCipherCache(newDecCiphers, activeUserId); + await this.setFailedDecryptedCiphers(failedCiphers, activeUserId); + + return newDecCiphers; } private async getDecryptedCiphers() { @@ -402,7 +430,17 @@ export class CipherService implements CipherServiceAbstraction { ); } - private async decryptCiphers(ciphers: Cipher[], userId: UserId) { + /** + * Decrypts the provided ciphers using the provided user's keys. + * @param ciphers + * @param userId + * @returns Two cipher arrays, the first containing successfully decrypted ciphers and the second containing ciphers that failed to decrypt. + * @private + */ + private async decryptCiphers( + ciphers: Cipher[], + userId: UserId, + ): Promise<[CipherView[], CipherView[]]> { const keys = await firstValueFrom(this.keyService.cipherDecryptionKeys$(userId, true)); if (keys == null || (keys.userKey == null && Object.keys(keys.orgKeys).length === 0)) { @@ -420,7 +458,7 @@ export class CipherService implements CipherServiceAbstraction { {} as Record, ); - const decCiphers = ( + const allCipherViews = ( await Promise.all( Object.entries(grouped).map(async ([orgId, groupedCiphers]) => { if (await this.configService.getFeatureFlag(FeatureFlag.PM4154_BulkEncryptionService)) { @@ -440,7 +478,18 @@ export class CipherService implements CipherServiceAbstraction { .flat() .sort(this.getLocaleSortingFunction()); - return decCiphers; + // Split ciphers into two arrays, one for successfully decrypted ciphers and one for ciphers that failed to decrypt + return allCipherViews.reduce( + (acc, c) => { + if (c.decryptionFailure) { + acc[1].push(c); + } else { + acc[0].push(c); + } + return acc; + }, + [[], []] as [CipherView[], CipherView[]], + ); } private async reindexCiphers() { @@ -1272,10 +1321,15 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; const ciphers = await firstValueFrom(this.cipherViews$); + const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$); if (!ciphers) { return encryptedCiphers; } + if (failedCiphers.length > 0) { + throw new Error("Cannot rotate ciphers when decryption failures are present"); + } + const userCiphers = ciphers.filter((c) => c.organizationId == null); if (userCiphers.length === 0) { return encryptedCiphers; @@ -1636,6 +1690,7 @@ export class CipherService implements CipherServiceAbstraction { private async clearDecryptedCiphersState(userId: UserId) { await this.setDecryptedCiphers(null, userId); + await this.setFailedDecryptedCiphers(null, userId); this.clearSortedCiphers(); } diff --git a/libs/common/src/vault/services/folder/folder-api.service.ts b/libs/common/src/vault/services/folder/folder-api.service.ts index 24831393668..9d434cc646d 100644 --- a/libs/common/src/vault/services/folder/folder-api.service.ts +++ b/libs/common/src/vault/services/folder/folder-api.service.ts @@ -1,6 +1,5 @@ -import { UserId } from "@bitwarden/common/types/guid"; - import { ApiService } from "../../../abstractions/api.service"; +import { UserId } from "../../../types/guid"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; import { FolderData } from "../../../vault/models/data/folder.data"; diff --git a/libs/common/src/vault/services/folder/folder.service.spec.ts b/libs/common/src/vault/services/folder/folder.service.spec.ts index 612cd83d99b..9fdb4327b98 100644 --- a/libs/common/src/vault/services/folder/folder.service.spec.ts +++ b/libs/common/src/vault/services/folder/folder.service.spec.ts @@ -1,6 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { makeEncString } from "../../../../spec"; import { FakeAccountService, mockAccountServiceWith } from "../../../../spec/fake-account-service"; diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 3aac5374fcb..c21a92fd894 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -2,9 +2,10 @@ // @ts-strict-ignore import { Observable, Subject, firstValueFrom, map, shareReplay, switchMap, merge } from "rxjs"; -import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - +import { EncryptService } from ".././../../platform/abstractions/encrypt.service"; +import { Utils } from ".././../../platform/misc/utils"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeyService } from "../../../../../key-management/src/abstractions/key.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; diff --git a/libs/common/src/vault/services/key-state/ciphers.state.ts b/libs/common/src/vault/services/key-state/ciphers.state.ts index d9cedb89027..d85e625243e 100644 --- a/libs/common/src/vault/services/key-state/ciphers.state.ts +++ b/libs/common/src/vault/services/key-state/ciphers.state.ts @@ -28,6 +28,15 @@ export const DECRYPTED_CIPHERS = UserKeyDefinition.record( }, ); +export const FAILED_DECRYPTED_CIPHERS = UserKeyDefinition.array( + CIPHERS_MEMORY, + "failedDecryptedCiphers", + { + deserializer: (cipher: Jsonify) => CipherView.fromJSON(cipher), + clearOn: ["logout", "lock"], + }, +); + export const LOCAL_DATA_KEY = new UserKeyDefinition>( CIPHERS_DISK_LOCAL, "localData", diff --git a/libs/common/tsconfig.json b/libs/common/tsconfig.json index 27d8acbd360..c28b60e28f8 100644 --- a/libs/common/tsconfig.json +++ b/libs/common/tsconfig.json @@ -1,5 +1,16 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/auth/common": ["../auth/src/common"], + // TODO: Remove once circular dependencies in admin-console, auth and key-management are resolved + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"] + } + }, "include": ["src", "spec", "./custom-matchers.d.ts", "../key-management/src/index.ts"], "exclude": ["node_modules", "dist"] } diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts index c8e9d3d2fe3..ef7ba68b65c 100644 --- a/libs/components/src/a11y/a11y-grid.directive.ts +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -86,6 +86,8 @@ export class A11yGridDirective implements AfterViewInit { }); this.getActiveCellContent().tabIndex = 0; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (error) { // eslint-disable-next-line no-console console.error("Unable to initialize grid"); diff --git a/libs/components/src/async-actions/in-forms.stories.ts b/libs/components/src/async-actions/in-forms.stories.ts index fb94e43b196..7ddb32da281 100644 --- a/libs/components/src/async-actions/in-forms.stories.ts +++ b/libs/components/src/async-actions/in-forms.stories.ts @@ -5,6 +5,8 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { delay, of } from "rxjs"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { ButtonModule } from "../button"; diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index ed3dfc4e134..3654442801c 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Meta, StoryObj } from "@storybook/angular"; import { ButtonComponent } from "./button.component"; @@ -107,13 +105,13 @@ export const DisabledWithAttribute: Story = { }; export const Block: Story = { - render: (args: ButtonComponent) => ({ + render: (args) => ({ props: args, template: ` [block]="true" Link - + block Link diff --git a/libs/components/src/checkbox/checkbox.stories.ts b/libs/components/src/checkbox/checkbox.stories.ts index c322f29b957..f3e5a5cb3f7 100644 --- a/libs/components/src/checkbox/checkbox.stories.ts +++ b/libs/components/src/checkbox/checkbox.stories.ts @@ -9,6 +9,8 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { I18nService } from "@bitwarden/common/src/platform/abstractions/i18n.service"; import { BadgeModule } from "../badge"; diff --git a/libs/components/src/chip-select/chip-select.component.ts b/libs/components/src/chip-select/chip-select.component.ts index 5b07d0bbb96..a653d79f83f 100644 --- a/libs/components/src/chip-select/chip-select.component.ts +++ b/libs/components/src/chip-select/chip-select.component.ts @@ -18,6 +18,8 @@ import { import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { compareValues } from "../../../common/src/platform/misc/compare-values"; import { ButtonModule } from "../button"; import { IconButtonModule } from "../icon-button"; diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 1525d2e0171..7cb6f40aa5b 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; import { NoopAnimationsModule } from "@angular/platform-browser/animations"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; @@ -78,7 +76,7 @@ export default { type Story = StoryObj; export const Default: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: args, template: ` @@ -142,7 +140,7 @@ export const Loading: Story = { }; export const ScrollingContent: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: args, template: ` @@ -197,7 +195,7 @@ export const TabContent: Story = { }; export const WithCards: Story = { - render: (args: DialogComponent) => ({ + render: (args) => ({ props: { formObj: new FormGroup({ name: new FormControl(""), diff --git a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts index 409d691beb0..4f21b8611b3 100644 --- a/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts +++ b/libs/components/src/dialog/simple-dialog/simple-configurable-dialog/simple-configurable-dialog.service.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { Component } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; @@ -72,7 +70,7 @@ class StoryDialogComponent { content: this.i18nService.t("dialogContent"), type: "primary", acceptButtonText: "Ok", - cancelButtonText: null, + cancelButtonText: undefined, }, { title: this.i18nService.t("primaryTypeSimpleDialog"), @@ -123,7 +121,7 @@ class StoryDialogComponent { showCallout = false; calloutType = "info"; - dialogCloseResult: boolean; + dialogCloseResult?: boolean; constructor( public dialogService: DialogService, diff --git a/libs/components/src/form-field/bit-validators.stories.ts b/libs/components/src/form-field/bit-validators.stories.ts index df021256400..642ff30bb5a 100644 --- a/libs/components/src/form-field/bit-validators.stories.ts +++ b/libs/components/src/form-field/bit-validators.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormsModule, ReactiveFormsModule, FormBuilder } from "@angular/forms"; import { StoryObj, Meta, moduleMetadata } from "@storybook/angular"; @@ -51,7 +49,7 @@ const template = ` `; export const ForbiddenCharacters: StoryObj = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: new FormBuilder().group({ name: ["", forbiddenCharacters(["\\", "/", "@", "#", "$", "%", "^", "&", "*", "(", ")"])], @@ -62,7 +60,7 @@ export const ForbiddenCharacters: StoryObj = { }; export const TrimValidator: StoryObj = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: new FormBuilder().group({ name: [ diff --git a/libs/components/src/form-field/form-field.stories.ts b/libs/components/src/form-field/form-field.stories.ts index a02158655ee..6d8323e088e 100644 --- a/libs/components/src/form-field/form-field.stories.ts +++ b/libs/components/src/form-field/form-field.stories.ts @@ -1,6 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { TextFieldModule } from "@angular/cdk/text-field"; +import { Directive, ElementRef, Input, OnInit, Renderer2 } from "@angular/core"; import { AbstractControl, UntypedFormBuilder, @@ -12,7 +13,6 @@ import { } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; -import { A11yTitleDirective } from "@bitwarden/angular/src/directives/a11y-title.directive"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { AsyncActionsModule } from "../async-actions"; @@ -31,6 +31,41 @@ import { I18nMockService } from "../utils/i18n-mock.service"; import { BitFormFieldComponent } from "./form-field.component"; import { FormFieldModule } from "./form-field.module"; +// TOOD: This solves a circular dependency between components and angular. +@Directive({ + selector: "[appA11yTitle]", +}) +export class A11yTitleDirective implements OnInit { + @Input() set appA11yTitle(title: string) { + this.title = title; + this.setAttributes(); + } + + private title: string; + private originalTitle: string | null; + private originalAriaLabel: string | null; + + constructor( + private el: ElementRef, + private renderer: Renderer2, + ) {} + + ngOnInit() { + this.originalTitle = this.el.nativeElement.getAttribute("title"); + this.originalAriaLabel = this.el.nativeElement.getAttribute("aria-label"); + this.setAttributes(); + } + + private setAttributes() { + if (this.originalTitle === null) { + this.renderer.setAttribute(this.el.nativeElement, "title", this.title); + } + if (this.originalAriaLabel === null) { + this.renderer.setAttribute(this.el.nativeElement, "aria-label", this.title); + } + } +} + export default { title: "Component Library/Form/Field", component: BitFormFieldComponent, @@ -190,7 +225,7 @@ export const Required: Story = { Label - + FormControl @@ -200,7 +235,7 @@ export const Required: Story = { }; export const Hint: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: { formObj: formObj, ...args, @@ -268,7 +303,7 @@ export const Readonly: Story = { Textarea Premium @@ -361,7 +396,7 @@ export const PartiallyDisabledButtonInputGroup: Story = { }; export const Select: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` @@ -377,7 +412,7 @@ export const Select: Story = { }; export const AdvancedSelect: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` @@ -422,7 +457,7 @@ export const FileInput: Story = { }; export const Textarea: Story = { - render: (args: BitFormFieldComponent) => ({ + render: (args) => ({ props: args, template: /*html*/ ` diff --git a/libs/components/src/navigation/nav-group.component.ts b/libs/components/src/navigation/nav-group.component.ts index 58d93ddd3a4..d615bfe0582 100644 --- a/libs/components/src/navigation/nav-group.component.ts +++ b/libs/components/src/navigation/nav-group.component.ts @@ -70,6 +70,8 @@ export class NavGroupComponent extends NavBaseComponent implements AfterContentI setOpen(isOpen: boolean) { this.open = isOpen; this.openChange.emit(this.open); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions this.open && this.parentNavGroup?.setOpen(this.open); } diff --git a/libs/components/src/search/search.stories.ts b/libs/components/src/search/search.stories.ts index 71c180c6d51..a6cd714d43a 100644 --- a/libs/components/src/search/search.stories.ts +++ b/libs/components/src/search/search.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; @@ -34,7 +32,7 @@ export default { type Story = StoryObj; export const Default: Story = { - render: (args: SearchComponent) => ({ + render: (args) => ({ props: args, template: ` diff --git a/libs/components/src/section/section-header.component.html b/libs/components/src/section/section-header.component.html index 3f96e22540f..f070cfeae02 100644 --- a/libs/components/src/section/section-header.component.html +++ b/libs/components/src/section/section-header.component.html @@ -2,7 +2,7 @@
-
+
diff --git a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts index 02b49a3e915..7709506f050 100644 --- a/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts +++ b/libs/components/src/stories/kitchen-sink/components/dialog-virtual-scroll-block.component.ts @@ -12,7 +12,7 @@ import { TableDataSource, TableModule } from "../../../table"; imports: [DialogModule, IconButtonModule, SectionComponent, TableModule, ScrollingModule], template: /*html*/ ` - + Id diff --git a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts index 44080e29049..a90597c1710 100644 --- a/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts +++ b/libs/components/src/stories/kitchen-sink/kitchen-sink.stories.ts @@ -196,4 +196,10 @@ export const VirtualScrollBlockingDialog: Story = { await userEvent.click(dialogButton); }, + parameters: { + chromatic: { + // TODO CL-524 fix flaky story (number of virtual scroll rows is inconsistent) + disableSnapshot: true, + }, + }, }; diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts index f0b55402194..5afed111c0f 100644 --- a/libs/components/src/toast/index.ts +++ b/libs/components/src/toast/index.ts @@ -1,2 +1,3 @@ export * from "./toast.module"; export * from "./toast.service"; +export type { ToastVariant } from "./toast.component"; diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts index 2ca1c0fa952..382e19097b0 100644 --- a/libs/components/src/toast/toast.stories.ts +++ b/libs/components/src/toast/toast.stories.ts @@ -1,5 +1,3 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; @@ -24,7 +22,7 @@ const toastServiceExampleTemplate = ` }) export class ToastServiceExampleComponent { @Input() - toastOptions: ToastOptions; + toastOptions?: ToastOptions; constructor(protected toastService: ToastService) {} } @@ -40,7 +38,7 @@ export default { }), applicationConfig({ providers: [ - ToastModule.forRoot().providers, + ToastModule.forRoot().providers!, { provide: I18nService, useFactory: () => { diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index 26616d07156..6e887030c34 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -11,6 +11,7 @@ module.exports = { content: [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/key-management/src/**/*.{html,ts}", "../../libs/auth/src/**/*.{html,ts}", ], safelist: [], diff --git a/libs/components/tsconfig.json b/libs/components/tsconfig.json index dabcecf78e9..71eef15fac4 100644 --- a/libs/components/tsconfig.json +++ b/libs/components/tsconfig.json @@ -20,7 +20,6 @@ "lib": ["es2020", "dom"], "paths": { "@bitwarden/common/*": ["../common/src/*"], - "@bitwarden/angular/*": ["../angular/src/*"], "@bitwarden/platform": ["../platform/src"] }, "plugins": [ diff --git a/libs/importer/jest.config.js b/libs/importer/jest.config.js index 68daba3d407..ab449dc7757 100644 --- a/libs/importer/jest.config.js +++ b/libs/importer/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../shared/jest.config.ts"); diff --git a/libs/importer/src/components/import.component.html b/libs/importer/src/components/import.component.html index 0da8127369e..25172f850b7 100644 --- a/libs/importer/src/components/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -395,6 +395,11 @@

{{ "data" | i18n }}

Open the FullClient, go to the Main Menu and select Export. Start the export passwords wizard and follow the instructions to export a CSV file.
+ + Log in to NordPass and open Settings → Scroll down to the Import and Export section + and select Export items → Enter your Master Password and select Continue. → Save + the CSV file on your device. + { function createCipher(options: Partial = {}) { const cipher = new CipherView(); - cipher.name; + cipher.name = options.name; cipher.type = options.type; cipher.folderId = options.folderId; cipher.collectionIds = options.collectionIds; diff --git a/libs/importer/tsconfig.json b/libs/importer/tsconfig.json index 6004a56fb55..2235cccb5c7 100644 --- a/libs/importer/tsconfig.json +++ b/libs/importer/tsconfig.json @@ -1,5 +1,21 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/libs/key-management/jest.config.js b/libs/key-management/jest.config.js index e20d02303d9..ad8023e906b 100644 --- a/libs/key-management/jest.config.js +++ b/libs/key-management/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/key-management/src/abstractions/key.service.ts b/libs/key-management/src/abstractions/key.service.ts index f5ab624bb22..da59c2986b2 100644 --- a/libs/key-management/src/abstractions/key.service.ts +++ b/libs/key-management/src/abstractions/key.service.ts @@ -5,13 +5,29 @@ import { Observable } from "rxjs"; import { EncryptedOrganizationKeyData } from "@bitwarden/common/admin-console/models/data/encrypted-organization-key.data"; import { KdfConfig } from "@bitwarden/key-management"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderOrganizationResponse } from "../../../common/src/admin-console/models/response/profile-provider-organization.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ProfileProviderResponse } from "../../../common/src/admin-console/models/response/profile-provider.response"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { KeySuffixOptions, HashPurpose } from "../../../common/src/platform/enums"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { EncryptedString, EncString } from "../../../common/src/platform/models/domain/enc-string"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { SymmetricCryptoKey } from "../../../common/src/platform/models/domain/symmetric-crypto-key"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { OrganizationId, UserId } from "../../../common/src/types/guid"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserKey, MasterKey, diff --git a/libs/key-management/src/angular/index.ts b/libs/key-management/src/angular/index.ts new file mode 100644 index 00000000000..1eb9b88b072 --- /dev/null +++ b/libs/key-management/src/angular/index.ts @@ -0,0 +1,6 @@ +/** + * This barrel file should only contain Angular exports + */ + +export { LockComponent } from "./lock/components/lock.component"; +export { LockComponentService, UnlockOptions } from "./lock/services/lock-component.service"; diff --git a/libs/auth/src/angular/lock/lock.component.html b/libs/key-management/src/angular/lock/components/lock.component.html similarity index 96% rename from libs/auth/src/angular/lock/lock.component.html rename to libs/key-management/src/angular/lock/components/lock.component.html index 5f5991c681e..7d9ed6124f6 100644 --- a/libs/auth/src/angular/lock/lock.component.html +++ b/libs/key-management/src/angular/lock/components/lock.component.html @@ -86,12 +86,13 @@

{{ "or" | i18n }}

- + + +
diff --git a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts index cec818b1cd6..7bcffd92399 100644 --- a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts @@ -1,5 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { DialogRef } from "@angular/cdk/dialog"; import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; @@ -34,6 +35,7 @@ export class CredentialGeneratorHistoryDialogComponent { private accountService: AccountService, private history: GeneratorHistoryService, private dialogService: DialogService, + private dialogRef: DialogRef, ) { this.accountService.activeAccount$ .pipe( @@ -52,7 +54,13 @@ export class CredentialGeneratorHistoryDialogComponent { .subscribe(this.hasHistory$); } - clear = async () => { + /** closes the dialog */ + protected close() { + this.dialogRef.close(); + } + + /** Launches clear history flow */ + protected async clear() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "clearGeneratorHistoryTitle" }, content: { key: "cleargGeneratorHistoryDescription" }, @@ -64,5 +72,5 @@ export class CredentialGeneratorHistoryDialogComponent { if (confirmed) { await this.history.clear(await firstValueFrom(this.userId$)); } - }; + } } diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index 3af9d12abd7..a2b204eaca4 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -22,8 +22,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { IntegrationId } from "@bitwarden/common/tools/integration"; import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { Option } from "@bitwarden/components/src/select/option"; +import { ToastService, Option } from "@bitwarden/components"; import { AlgorithmInfo, CredentialAlgorithm, diff --git a/libs/tools/generator/components/src/password-generator.component.ts b/libs/tools/generator/components/src/password-generator.component.ts index 7e230d8bfd6..85363412ffa 100644 --- a/libs/tools/generator/components/src/password-generator.component.ts +++ b/libs/tools/generator/components/src/password-generator.component.ts @@ -19,8 +19,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { Option } from "@bitwarden/components/src/select/option"; +import { ToastService, Option } from "@bitwarden/components"; import { CredentialGeneratorService, Generators, @@ -94,6 +93,10 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + async ngOnInit() { if (this.userId) { this.userId$.next(this.userId); @@ -186,6 +189,7 @@ export class PasswordGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); diff --git a/libs/tools/generator/components/src/username-generator.component.ts b/libs/tools/generator/components/src/username-generator.component.ts index 63293ff7fd9..63c1adc602b 100644 --- a/libs/tools/generator/components/src/username-generator.component.ts +++ b/libs/tools/generator/components/src/username-generator.component.ts @@ -23,8 +23,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { IntegrationId } from "@bitwarden/common/tools/integration"; import { UserId } from "@bitwarden/common/types/guid"; -import { ToastService } from "@bitwarden/components"; -import { Option } from "@bitwarden/components/src/select/option"; +import { ToastService, Option } from "@bitwarden/components"; import { AlgorithmInfo, CredentialAlgorithm, @@ -79,6 +78,10 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { @Output() readonly onGenerated = new EventEmitter(); + /** emits algorithm info when the selected algorithm changes */ + @Output() + readonly onAlgorithm = new EventEmitter(); + /** Removes bottom margin from internal elements */ @Input({ transform: coerceBooleanProperty }) disableMargin = false; @@ -248,6 +251,7 @@ export class UsernameGeneratorComponent implements OnInit, OnDestroy { // template bindings refresh immediately this.zone.run(() => { this.algorithm$.next(algorithm); + this.onAlgorithm.next(algorithm); }); }); diff --git a/libs/tools/generator/components/tsconfig.json b/libs/tools/generator/components/tsconfig.json index c52bfd7b0df..76b060334f6 100644 --- a/libs/tools/generator/components/tsconfig.json +++ b/libs/tools/generator/components/tsconfig.json @@ -1,5 +1,18 @@ { - "extends": "../../../shared/tsconfig.libs", + "extends": "../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../admin-console/src/common"], + "@bitwarden/angular/*": ["../../../angular/src/*"], + "@bitwarden/auth/common": ["../../../auth/src/common"], + "@bitwarden/common/*": ["../../../common/src/*"], + "@bitwarden/components": ["../../../components/src"], + "@bitwarden/generator-core": ["../../../tools/generator/core/src"], + "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], + "@bitwarden/key-management": ["../../../key-management/src"], + "@bitwarden/platform": ["../../../platform/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/tools/generator/core/jest.config.js b/libs/tools/generator/core/jest.config.js index 71ccbc80b67..b052672c4af 100644 --- a/libs/tools/generator/core/jest.config.js +++ b/libs/tools/generator/core/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../../../shared/tsconfig.libs"); +const { compilerOptions } = require("../../../shared/tsconfig.spec"); /** @type {import('jest').Config} */ module.exports = { diff --git a/libs/tools/generator/core/src/data/generators.ts b/libs/tools/generator/core/src/data/generators.ts index afd7da6499d..92b20b02b75 100644 --- a/libs/tools/generator/core/src/data/generators.ts +++ b/libs/tools/generator/core/src/data/generators.ts @@ -58,6 +58,7 @@ const PASSPHRASE: CredentialGeneratorConfiguration< generateKey: "generatePassphrase", generatedValueKey: "passphrase", copyKey: "copyPassphrase", + useGeneratedValueKey: "useThisPassphrase", onlyOnRequest: false, request: [], engine: { @@ -119,6 +120,7 @@ const PASSWORD: CredentialGeneratorConfiguration< generateKey: "generatePassword", generatedValueKey: "password", copyKey: "copyPassword", + useGeneratedValueKey: "useThisPassword", onlyOnRequest: false, request: [], engine: { @@ -195,6 +197,7 @@ const USERNAME: CredentialGeneratorConfiguration = { @@ -83,6 +84,7 @@ const SomeConfiguration: CredentialGeneratorConfiguration { let fixture: ComponentFixture; let sendListFiltersService: SendListFiltersService; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; + const userId = "userId" as UserId; beforeEach(async () => { sendListFiltersService = new SendListFiltersService(mock(), new FormBuilder()); sendListFiltersService.resetFilterForm = jest.fn(); billingAccountProfileStateService = mock(); + accountService = mock(); - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + accountService.activeAccount$ = of({ + id: userId, + email: "test@email.com", + emailVerified: true, + name: "Test User", + }); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); await TestBed.configureTestingModule({ imports: [ @@ -37,10 +48,8 @@ describe("SendListFiltersComponent", () => { providers: [ { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: SendListFiltersService, useValue: sendListFiltersService }, - { - provide: BillingAccountProfileStateService, - useValue: billingAccountProfileStateService, - }, + { provide: BillingAccountProfileStateService, useValue: billingAccountProfileStateService }, + { provide: AccountService, useValue: accountService }, ], }).compileComponents(); @@ -57,6 +66,7 @@ describe("SendListFiltersComponent", () => { let canAccessPremium: boolean | undefined; component["canAccessPremium$"].subscribe((value) => (canAccessPremium = value)); expect(canAccessPremium).toBe(true); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith(userId); }); it("should call resetFilterForm on ngOnDestroy", () => { diff --git a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts index b313ced742a..d42eab382e9 100644 --- a/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts +++ b/libs/tools/send/send-ui/src/send-list-filters/send-list-filters.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy } from "@angular/core"; import { ReactiveFormsModule } from "@angular/forms"; -import { Observable } from "rxjs"; +import { Observable, of, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; -import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ChipSelectComponent } from "@bitwarden/components"; import { SendListFiltersService } from "../services/send-list-filters.service"; @@ -23,8 +24,15 @@ export class SendListFiltersComponent implements OnDestroy { constructor( private sendListFiltersService: SendListFiltersService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { - this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; + this.canAccessPremium$ = accountService.activeAccount$.pipe( + switchMap((account) => + account + ? billingAccountProfileStateService.hasPremiumFromAnySource$(account.id) + : of(false), + ), + ); } ngOnDestroy(): void { diff --git a/libs/tools/send/send-ui/tsconfig.json b/libs/tools/send/send-ui/tsconfig.json index c52bfd7b0df..671154e0a04 100644 --- a/libs/tools/send/send-ui/tsconfig.json +++ b/libs/tools/send/send-ui/tsconfig.json @@ -1,5 +1,21 @@ { - "extends": "../../../shared/tsconfig.libs", + "extends": "../../../shared/tsconfig", + "compilerOptions": { + "paths": { + "@bitwarden/admin-console/common": ["../../../admin-console/src/common"], + "@bitwarden/angular/*": ["../../../angular/src/*"], + "@bitwarden/auth/common": ["../../../auth/src/common"], + "@bitwarden/common/*": ["../../../common/src/*"], + "@bitwarden/components": ["../../../components/src"], + "@bitwarden/generator-components": ["../../../tools/generator/components/src"], + "@bitwarden/generator-core": ["../../../tools/generator/core/src"], + "@bitwarden/generator-history": ["../../../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../../../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../../../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../../../key-management/src"], + "@bitwarden/platform": ["../../../platform/src"] + } + }, "include": ["src"], "exclude": ["node_modules", "dist"] } diff --git a/libs/vault/jest.config.js b/libs/vault/jest.config.js index e960eed9f1a..e33c115e8dd 100644 --- a/libs/vault/jest.config.js +++ b/libs/vault/jest.config.js @@ -1,6 +1,6 @@ const { pathsToModuleNameMapper } = require("ts-jest"); -const { compilerOptions } = require("../shared/tsconfig.libs"); +const { compilerOptions } = require("../shared/tsconfig.spec"); const sharedConfig = require("../../libs/shared/jest.config.angular"); diff --git a/libs/vault/src/cipher-form/cipher-form.stories.ts b/libs/vault/src/cipher-form/cipher-form.stories.ts index 72c4acb23cd..f7146776b73 100644 --- a/libs/vault/src/cipher-form/cipher-form.stories.ts +++ b/libs/vault/src/cipher-form/cipher-form.stories.ts @@ -31,7 +31,8 @@ import { PasswordRepromptService, } from "@bitwarden/vault"; // FIXME: remove `/apps` import from `/libs` -// eslint-disable-next-line import/no-restricted-paths +// FIXME: remove `src` and fix import +// eslint-disable-next-line import/no-restricted-paths, no-restricted-imports import { PreloadedEnglishI18nModule } from "@bitwarden/web-vault/src/app/core/tests"; import { CipherFormService } from "./abstractions/cipher-form.service"; diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html index 445908679c3..16bccebb939 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.html @@ -2,9 +2,11 @@ *ngIf="type === 'password'" [disableMargin]="disableMargin" (onGenerated)="onCredentialGenerated($event)" + (onAlgorithm)="algorithm($event)" > diff --git a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts index 1a2bac2ce50..fdde5e15d91 100644 --- a/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts +++ b/libs/vault/src/cipher-form/components/cipher-generator/cipher-form-generator.component.ts @@ -5,7 +5,7 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; import { GeneratorModule } from "@bitwarden/generator-components"; -import { GeneratedCredential } from "@bitwarden/generator-core"; +import { AlgorithmInfo, GeneratedCredential } from "@bitwarden/generator-core"; /** * Renders a password or username generator UI and emits the most recently generated value. @@ -18,6 +18,9 @@ import { GeneratedCredential } from "@bitwarden/generator-core"; imports: [CommonModule, GeneratorModule], }) export class CipherFormGeneratorComponent { + @Input() + algorithm: (selected: AlgorithmInfo) => void; + /** * The type of generator form to show. */ diff --git a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts index f63a974d487..62d171b8436 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/add-edit-custom-field-dialog/add-edit-custom-field-dialog.component.spec.ts @@ -38,6 +38,8 @@ describe("AddEditCustomFieldDialogComponent", () => { fixture = TestBed.createComponent(AddEditCustomFieldDialogComponent); component = fixture.componentInstance; + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-expressions fixture.detectChanges; }); diff --git a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts index 0c0fa1b4184..a5e57d2858f 100644 --- a/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts +++ b/libs/vault/src/cipher-form/components/custom-fields/custom-fields.component.spec.ts @@ -19,6 +19,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { DialogService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "../../../../../components/src/form-field/password-input-toggle.directive"; import { CipherFormConfig } from "../../abstractions/cipher-form-config.service"; import { CipherFormContainer } from "../../cipher-form-container"; diff --git a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts index 182427f7f42..69cfd419742 100644 --- a/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts +++ b/libs/vault/src/cipher-form/components/login-details-section/login-details-section.component.spec.ts @@ -14,6 +14,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { CipherFormGenerationService } from "../../abstractions/cipher-form-generation.service"; diff --git a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts index 914654c522d..134897c9356 100644 --- a/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts +++ b/libs/vault/src/cipher-form/components/sshkey-section/sshkey-section.component.ts @@ -2,11 +2,15 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view"; import { CardComponent, FormFieldModule, @@ -16,6 +20,7 @@ import { SelectModule, TypographyModule, } from "@bitwarden/components"; +import { generate_ssh_key } from "@bitwarden/sdk-internal"; import { CipherFormContainer } from "../../cipher-form-container"; @@ -50,20 +55,35 @@ export class SshKeySectionComponent implements OnInit { * leaving as just null gets inferred as `unknown` */ sshKeyForm = this.formBuilder.group({ - privateKey: null as string | null, - publicKey: null as string | null, - keyFingerprint: null as string | null, + privateKey: [""], + publicKey: [""], + keyFingerprint: [""], }); constructor( private cipherFormContainer: CipherFormContainer, private formBuilder: FormBuilder, private i18nService: I18nService, - ) {} + private sdkService: SdkService, + ) { + this.cipherFormContainer.registerChildForm("sshKeyDetails", this.sshKeyForm); + this.sshKeyForm.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => { + const data = new SshKeyView(); + data.privateKey = value.privateKey; + data.publicKey = value.publicKey; + data.keyFingerprint = value.keyFingerprint; + this.cipherFormContainer.patchCipher((cipher) => { + cipher.sshKey = data; + return cipher; + }); + }); + } - ngOnInit() { - if (this.originalCipherView?.card) { + async ngOnInit() { + if (this.originalCipherView?.sshKey) { this.setInitialValues(); + } else { + await this.generateSshKey(); } this.sshKeyForm.disable(); @@ -79,4 +99,14 @@ export class SshKeySectionComponent implements OnInit { keyFingerprint, }); } + + private async generateSshKey() { + await firstValueFrom(this.sdkService.client$); + const sshKey = generate_ssh_key("Ed25519"); + this.sshKeyForm.setValue({ + privateKey: sshKey.private_key, + publicKey: sshKey.public_key, + keyFingerprint: sshKey.key_fingerprint, + }); + } } diff --git a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts index acfdfa3337d..0c2ca35cbbc 100644 --- a/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts +++ b/libs/vault/src/cipher-view/attachments/attachments-v2-view.component.ts @@ -6,6 +6,7 @@ import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { NEVER, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { StateProvider } from "@bitwarden/common/platform/state"; import { OrganizationId } from "@bitwarden/common/types/guid"; @@ -47,16 +48,22 @@ export class AttachmentsV2ViewComponent { private keyService: KeyService, private billingAccountProfileStateService: BillingAccountProfileStateService, private stateProvider: StateProvider, + private accountService: AccountService, ) { this.subscribeToHasPremiumCheck(); this.subscribeToOrgKey(); } subscribeToHasPremiumCheck() { - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntilDestroyed()) - .subscribe((data) => { - this.canAccessPremium = data; + this.accountService.activeAccount$ + .pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + takeUntilDestroyed(), + ) + .subscribe((hasPremium) => { + this.canAccessPremium = hasPremium; }); } diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html index 8503604bf7c..b4a0d4841f8 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.html @@ -116,7 +116,7 @@

{{ "loginCredentials" | i18n }}

{{ "verificationCodeTotp" | i18n }} {{ "loginCredentials" | i18n }} id="totp" readonly bitInput - [type]="!(isPremium$ | async) ? 'password' : 'text'" + [type]="!(allowTotpGeneration$ | async) ? 'password' : 'text'" [value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'" aria-readonly="true" data-testid="login-totp" class="tw-font-mono" />
{{ "loginCredentials" | i18n }} showToast [appA11yTitle]="'copyVerificationCode' | i18n" data-testid="copy-totp" - [disabled]="!(isPremium$ | async)" + [disabled]="!(allowTotpGeneration$ | async)" class="disabled:tw-cursor-default" > diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts index c8ac0598c97..9bb96d1accd 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.spec.ts @@ -6,17 +6,23 @@ import { BehaviorSubject } from "rxjs"; import { CopyClickDirective } from "@bitwarden/angular/directives/copy-click.directive"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { Fido2CredentialView } from "@bitwarden/common/vault/models/view/fido2-credential.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { BitFormFieldComponent, ToastService } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitPasswordInputToggleDirective } from "@bitwarden/components/src/form-field/password-input-toggle.directive"; import { LoginCredentialsViewComponent } from "./login-credentials-view.component"; @@ -26,6 +32,17 @@ describe("LoginCredentialsViewComponent", () => { let fixture: ComponentFixture; const hasPremiumFromAnySource$ = new BehaviorSubject(true); + const mockAccount = { + id: "test-user-id" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + type: 0, + status: 0, + kdf: 0, + kdfIterations: 0, + }; + const activeAccount$ = new BehaviorSubject(mockAccount); const cipher = { id: "cipher-id", @@ -48,8 +65,11 @@ describe("LoginCredentialsViewComponent", () => { providers: [ { provide: BillingAccountProfileStateService, - useValue: mock({ hasPremiumFromAnySource$ }), + useValue: mock({ + hasPremiumFromAnySource$: () => hasPremiumFromAnySource$, + }), }, + { provide: AccountService, useValue: mock({ activeAccount$ }) }, { provide: PremiumUpgradePromptService, useValue: mock() }, { provide: EventCollectionService, useValue: mock({ collect }) }, { provide: PlatformUtilsService, useValue: mock() }, diff --git a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts index 0c42c2ddda7..281e187f78b 100644 --- a/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts +++ b/libs/vault/src/cipher-view/login-credentials/login-credentials-view.component.ts @@ -2,24 +2,35 @@ // @ts-strict-ignore import { CommonModule, DatePipe } from "@angular/common"; import { Component, inject, Input } from "@angular/core"; -import { Observable, shareReplay } from "rxjs"; +import { + BehaviorSubject, + combineLatest, + filter, + map, + Observable, + shareReplay, + switchMap, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { + BadgeModule, + ColorPasswordModule, FormFieldModule, + IconButtonModule, SectionComponent, SectionHeaderComponent, TypographyModule, - IconButtonModule, - BadgeModule, - ColorPasswordModule, } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { PremiumUpgradePromptService } from "../../../../../libs/common/src/vault/abstractions/premium-upgrade-prompt.service"; import { BitTotpCountdownComponent } from "../../components/totp-countdown/totp-countdown.component"; import { ReadOnlyCipherCardComponent } from "../read-only-cipher-card/read-only-cipher-card.component"; @@ -48,12 +59,31 @@ type TotpCodeValues = { ], }) export class LoginCredentialsViewComponent { - @Input() cipher: CipherView; + @Input() + get cipher(): CipherView { + return this._cipher$.value; + } + set cipher(value: CipherView) { + this._cipher$.next(value); + } + private _cipher$ = new BehaviorSubject(null); - isPremium$: Observable = - this.billingAccountProfileStateService.hasPremiumFromAnySource$.pipe( - shareReplay({ refCount: true, bufferSize: 1 }), - ); + private _userHasPremium$: Observable = this.accountService.activeAccount$.pipe( + switchMap((account) => + this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id), + ), + ); + + allowTotpGeneration$: Observable = combineLatest([ + this._userHasPremium$, + this._cipher$.pipe(filter((c) => c != null)), + ]).pipe( + map(([userHasPremium, cipher]) => { + // User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers + return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp; + }), + shareReplay({ refCount: true, bufferSize: 1 }), + ); showPasswordCount: boolean = false; passwordRevealed: boolean = false; totpCodeCopyObj: TotpCodeValues; @@ -64,6 +94,7 @@ export class LoginCredentialsViewComponent { private i18nService: I18nService, private premiumUpgradeService: PremiumUpgradePromptService, private eventCollectionService: EventCollectionService, + private accountService: AccountService, ) {} get fido2CredentialCreationDateValue(): string { diff --git a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts index 4c84b2852a8..21f82234b2c 100644 --- a/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts +++ b/libs/vault/src/cipher-view/view-identity-sections/view-identity-sections.component.spec.ts @@ -8,6 +8,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { IdentityView } from "@bitwarden/common/vault/models/view/identity.view"; import { SectionHeaderComponent } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { BitInputDirective } from "../../../../components/src/input/input.directive"; import { ViewIdentitySectionsComponent } from "./view-identity-sections.component"; diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html new file mode 100644 index 00000000000..4a0d60d20e3 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.html @@ -0,0 +1,32 @@ + + + {{ "decryptionError" | i18n }} +
+

+ {{ "couldNotDecryptVaultItemsBelow" | i18n }} + {{ + "contactCSToAvoidDataLossPart1" | i18n + }} + {{ "contactCSToAvoidDataLossPart2" | i18n }} +

+
    +
  • + {{ id }} +
  • +
+
+ + + +
diff --git a/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts new file mode 100644 index 00000000000..c183c4bb246 --- /dev/null +++ b/libs/vault/src/components/decryption-failure-dialog/decryption-failure-dialog.component.ts @@ -0,0 +1,59 @@ +import { DIALOG_DATA, DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component, inject } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { + AnchorLinkDirective, + AsyncActionsModule, + ButtonModule, + DialogModule, + DialogService, + TypographyModule, +} from "@bitwarden/components"; + +export type DecryptionFailureDialogParams = { + cipherIds: CipherId[]; +}; + +@Component({ + standalone: true, + selector: "vault-decryption-failure-dialog", + templateUrl: "./decryption-failure-dialog.component.html", + imports: [ + DialogModule, + CommonModule, + TypographyModule, + JslibModule, + AsyncActionsModule, + ButtonModule, + AnchorLinkDirective, + ], +}) +export class DecryptionFailureDialogComponent { + protected dialogRef = inject(DialogRef); + protected params = inject(DIALOG_DATA); + protected platformUtilsService = inject(PlatformUtilsService); + + selectText(element: HTMLElement) { + const selection = window.getSelection(); + if (selection == null) { + return; + } + selection.removeAllRanges(); + const range = document.createRange(); + range.selectNodeContents(element); + selection.addRange(range); + } + + openContactSupport(event: Event) { + event.preventDefault(); + this.platformUtilsService.launchUri("https://bitwarden.com/contact"); + } + + static open(dialogService: DialogService, params: DecryptionFailureDialogParams) { + return dialogService.open(DecryptionFailureDialogComponent, { data: params }); + } +} diff --git a/libs/vault/src/components/download-attachment/download-attachment.component.ts b/libs/vault/src/components/download-attachment/download-attachment.component.ts index b22ced3d899..a47cdc8d1d5 100644 --- a/libs/vault/src/components/download-attachment/download-attachment.component.ts +++ b/libs/vault/src/components/download-attachment/download-attachment.component.ts @@ -98,6 +98,8 @@ export class DownloadAttachmentComponent { fileName: this.attachment.fileName, blobData: decBuf, }); + // FIXME: Remove when updating file. Eslint update + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { this.toastService.showToast({ variant: "error", diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html index ddff560fd00..9d7808379d3 100644 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.html @@ -1,12 +1,7 @@

{{ "newDeviceVerificationNoticeContentPage1" | i18n }} - + {{ "learnMore" | i18n }}.

diff --git a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts index 8127c368046..8db923fec88 100644 --- a/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts +++ b/libs/vault/src/components/new-device-verification-notice/new-device-verification-notice-page-one.component.ts @@ -121,4 +121,10 @@ export class NewDeviceVerificationNoticePageOneComponent implements OnInit, Afte await this.router.navigate(["/vault"]); }; + + navigateToNewDeviceVerificationHelp(event: Event) { + event.preventDefault(); + + this.platformUtilsService.launchUri("https://bitwarden.com/help/new-device-verification/"); + } } diff --git a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts index 461cc734d76..fb90df4d6ac 100644 --- a/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts +++ b/libs/vault/src/components/password-history-view/password-history-view.component.spec.ts @@ -10,6 +10,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ColorPasswordModule, ItemModule } from "@bitwarden/components"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component"; import { PasswordHistoryViewComponent } from "./password-history-view.component"; diff --git a/libs/vault/src/index.ts b/libs/vault/src/index.ts index 0112de44241..c9a719934ac 100644 --- a/libs/vault/src/index.ts +++ b/libs/vault/src/index.ts @@ -16,5 +16,6 @@ export { DownloadAttachmentComponent } from "./components/download-attachment/do export { PasswordHistoryViewComponent } from "./components/password-history-view/password-history-view.component"; export { NewDeviceVerificationNoticePageOneComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-one.component"; export { NewDeviceVerificationNoticePageTwoComponent } from "./components/new-device-verification-notice/new-device-verification-notice-page-two.component"; +export { DecryptionFailureDialogComponent } from "./components/decryption-failure-dialog/decryption-failure-dialog.component"; export * as VaultIcons from "./icons"; diff --git a/libs/vault/src/services/copy-cipher-field.service.spec.ts b/libs/vault/src/services/copy-cipher-field.service.spec.ts index 48510b2efd9..5a273c0828f 100644 --- a/libs/vault/src/services/copy-cipher-field.service.spec.ts +++ b/libs/vault/src/services/copy-cipher-field.service.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { of } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService, Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -22,6 +23,8 @@ describe("CopyCipherFieldService", () => { let totpService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let accountService: MockProxy; + const userId = "userId"; beforeEach(() => { platformUtilsService = mock(); @@ -31,6 +34,9 @@ describe("CopyCipherFieldService", () => { totpService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + accountService = mock(); + + accountService.activeAccount$ = of({ id: userId } as Account); service = new CopyCipherFieldService( platformUtilsService, @@ -40,6 +46,7 @@ describe("CopyCipherFieldService", () => { totpService, i18nService, billingAccountProfileStateService, + accountService, ); }); @@ -128,12 +135,15 @@ describe("CopyCipherFieldService", () => { }); it("should get TOTP code when allowed from premium", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(true); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(true)); totpService.getCode.mockResolvedValue("123456"); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeTruthy(); expect(totpService.getCode).toHaveBeenCalledWith(valueToCopy); expect(platformUtilsService.copyToClipboard).toHaveBeenCalledWith("123456"); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( + userId, + ); }); it("should get TOTP code when allowed from organization", async () => { @@ -146,11 +156,14 @@ describe("CopyCipherFieldService", () => { }); it("should return early when the user is not allowed to use TOTP", async () => { - billingAccountProfileStateService.hasPremiumFromAnySource$ = of(false); + billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false)); const result = await service.copy(valueToCopy, actionType, cipher, skipReprompt); expect(result).toBeFalsy(); expect(totpService.getCode).not.toHaveBeenCalled(); expect(platformUtilsService.copyToClipboard).not.toHaveBeenCalled(); + expect(billingAccountProfileStateService.hasPremiumFromAnySource$).toHaveBeenCalledWith( + userId, + ); }); it("should return early when TOTP is not set", async () => { diff --git a/libs/vault/src/services/copy-cipher-field.service.ts b/libs/vault/src/services/copy-cipher-field.service.ts index bfcf3495865..2805f3e7541 100644 --- a/libs/vault/src/services/copy-cipher-field.service.ts +++ b/libs/vault/src/services/copy-cipher-field.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@angular/core"; import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -87,6 +88,7 @@ export class CopyCipherFieldService { private totpService: TotpService, private i18nService: I18nService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private accountService: AccountService, ) {} /** @@ -148,10 +150,16 @@ export class CopyCipherFieldService { * Determines if TOTP generation is allowed for a cipher and user. */ async totpAllowed(cipher: CipherView): Promise { + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (!activeAccount?.id) { + return false; + } return ( (cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || - (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumFromAnySource$))) + (await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeAccount.id), + ))) ); } } diff --git a/libs/vault/src/services/password-reprompt.service.spec.ts b/libs/vault/src/services/password-reprompt.service.spec.ts index 300b98215c2..7d0a834b2d6 100644 --- a/libs/vault/src/services/password-reprompt.service.spec.ts +++ b/libs/vault/src/services/password-reprompt.service.spec.ts @@ -1,5 +1,7 @@ import { MockProxy, mock } from "jest-mock-extended"; +// FIXME: remove `src` and fix import +// eslint-disable-next-line no-restricted-imports import { UserVerificationService } from "@bitwarden/common/src/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DialogService } from "@bitwarden/components"; diff --git a/libs/vault/tsconfig.json b/libs/vault/tsconfig.json index 6004a56fb55..8318212e81d 100644 --- a/libs/vault/tsconfig.json +++ b/libs/vault/tsconfig.json @@ -1,5 +1,23 @@ { - "extends": "../shared/tsconfig.libs", + "extends": "../shared/tsconfig", + "compilerOptions": { + "resolveJsonModule": true, + "paths": { + "@bitwarden/admin-console/common": ["../admin-console/src/common"], + "@bitwarden/angular/*": ["../angular/src/*"], + "@bitwarden/auth/common": ["../auth/src/common"], + "@bitwarden/common/*": ["../common/src/*"], + "@bitwarden/components": ["../components/src"], + "@bitwarden/generator-components": ["../tools/generator/components/src"], + "@bitwarden/generator-core": ["../tools/generator/core/src"], + "@bitwarden/generator-history": ["../tools/generator/extensions/history/src"], + "@bitwarden/generator-legacy": ["../tools/generator/extensions/legacy/src"], + "@bitwarden/generator-navigation": ["../tools/generator/extensions/navigation/src"], + "@bitwarden/key-management": ["../key-management/src"], + "@bitwarden/platform": ["../platform/src"], + "@bitwarden/vault": ["../vault/src"] + } + }, "include": ["src", "spec"], "exclude": ["node_modules", "dist"] } diff --git a/package-lock.json b/package-lock.json index c60d71881a5..87e0354f7ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@angular/router": "18.2.13", "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", + "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", @@ -37,7 +38,7 @@ "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", @@ -50,6 +51,7 @@ "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", + "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", @@ -68,7 +70,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.71", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" @@ -81,7 +83,6 @@ "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", - "@angular/elements": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", @@ -111,7 +112,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.2", + "@types/node": "22.10.5", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -119,8 +120,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.19.1", + "@typescript-eslint/parser": "8.19.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", @@ -128,11 +129,11 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "11.20.2", - "concurrently": "9.1.0", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "33.3.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", @@ -164,7 +165,7 @@ "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.1", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", @@ -190,11 +191,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2025.1.0" + "version": "2025.1.1" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2025.1.0", + "version": "2025.1.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@koa/multer": "3.0.2", @@ -221,7 +222,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.69", + "tldts": "6.1.71", "zxcvbn": "4.4.2" }, "bin": { @@ -230,7 +231,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2025.1.0", + "version": "2025.1.2", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -244,7 +245,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2025.1.0" + "version": "2025.1.1" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -2285,22 +2286,6 @@ "zone.js": "~0.14.10" } }, - "node_modules/@angular/elements": { - "version": "18.2.13", - "resolved": "https://registry.npmjs.org/@angular/elements/-/elements-18.2.13.tgz", - "integrity": "sha512-yahRkXWgFolpWMeVsTIlWYwoq4Bsz6wrfS4b+TKHIZbTCyRUlJ5zBFecpYMwgmVuQDJZp+WkUWZV2Qg7kLJR5w==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "18.2.13", - "rxjs": "^6.5.3 || ^7.4.0" - } - }, "node_modules/@angular/forms": { "version": "18.2.13", "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.13.tgz", @@ -4344,7 +4329,6 @@ "version": "7.25.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -5650,6 +5634,109 @@ "node": "*" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/css": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz", + "integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==", + "license": "MIT", + "dependencies": { + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/darwin-arm64": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", @@ -5836,6 +5923,50 @@ "lit": "^2.1.3" } }, + "node_modules/@figspec/components/node_modules/@lit/reactive-element": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", + "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.0.0" + } + }, + "node_modules/@figspec/components/node_modules/lit": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", + "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.6.0", + "lit-element": "^3.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@figspec/components/node_modules/lit-element": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", + "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.1.0", + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.8.0" + } + }, + "node_modules/@figspec/components/node_modules/lit-html": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", + "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, "node_modules/@figspec/react": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@figspec/react/-/react-1.0.3.tgz", @@ -6971,17 +7102,15 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.1.tgz", "integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ==", - "dev": true, "license": "BSD-3-Clause" }, "node_modules/@lit/reactive-element": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz", - "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==", - "dev": true, + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.4.tgz", + "integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.0.0" + "@lit-labs/ssr-dom-shim": "^1.2.0" } }, "node_modules/@lmdb/lmdb-darwin-arm64": { @@ -9626,9 +9755,9 @@ } }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz", + "integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -9702,7 +9831,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", - "dev": true, "license": "MIT" }, "node_modules/@types/plist": { @@ -9871,7 +9999,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, "license": "MIT" }, "node_modules/@types/unist": { @@ -9955,88 +10082,94 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", - "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.19.1.tgz", + "integrity": "sha512-tJzcVyvvb9h/PB96g30MpxACd9IrunT7GF9wfA9/0TJ1LxGOJx1TdPzSbBBnNED7K9Ka8ybJsnEpiXPktolTLg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/type-utils": "7.16.1", - "@typescript-eslint/utils": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/type-utils": "8.19.1", + "@typescript-eslint/utils": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", - "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.19.1.tgz", + "integrity": "sha512-Rp7k9lhDKBMRJB/nM9Ksp1zs4796wVNyihG9/TU9R6KCJDNkQbc2EOKjrBtLYh3396ZdpXLtr/MkaSEmNMtykw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/utils": "7.16.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/utils": "8.19.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", - "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.19.1.tgz", + "integrity": "sha512-IxG5gLO0Ne+KaUc8iW1A+XuKLd63o4wlbI1Zp692n1xojCl/THvgIKXJXBZixTh5dd5+yTJ/VXH7GJaaw21qXA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1" + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/@typescript-eslint/experimental-utils": { @@ -10189,46 +10322,42 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", - "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.19.1.tgz", + "integrity": "sha512-67gbfv8rAwawjYx3fYArwldTQKoYfezNUT4D5ioWetr/xCrxXxvleo3uuiFuKfejipvq+og7mjz3b0G2bVyUCw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.1", - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/typescript-estree": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/scope-manager": "8.19.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/typescript-estree": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", - "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.19.1.tgz", + "integrity": "sha512-60L9KIuN/xgmsINzonOcMDSB8p82h95hoBfSBtXuO4jlR1R9L1xSkmVZKgCPVfavDlXihh4ARNjXhh1gGnLC7Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1" + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -10236,13 +10365,13 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", - "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.19.1.tgz", + "integrity": "sha512-JBVHMLj7B1K1v1051ZaMMgLW4Q/jre5qGK0Ew6UgXz1Rqh+/xPzV1aW581OM00X6iOfyr1be+QyW8LOUf19BbA==", "dev": true, "license": "MIT", "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -10250,32 +10379,43 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", - "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.19.1.tgz", + "integrity": "sha512-jk/TZwSMJlxlNnqhy0Eod1PNEvCkpY6MXOXE/WLlblZ6ibb32i2We4uByoKPv1d0OD2xebDv4hbs3fm11SMw8Q==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "@typescript-eslint/visitor-keys": "7.16.1", + "@typescript-eslint/types": "8.19.1", + "@typescript-eslint/visitor-keys": "8.19.1", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/@typescript-eslint/utils": { @@ -10386,23 +10526,36 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", - "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", + "version": "8.19.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.19.1.tgz", + "integrity": "sha512-fzmjU8CHK853V/avYZAvuVut3ZTfwN5YtMaoi+X9Y9MA9keaWNHC3zEQ9zvyX/7Hj+5JkNyK1l7TOR2hevHB6Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.19.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -11961,6 +12114,46 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/babel-plugin-macros/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", @@ -12535,9 +12728,9 @@ "license": "MIT" }, "node_modules/bufferutil": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", - "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -12909,7 +13102,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -13642,9 +13834,9 @@ } }, "node_modules/concurrently": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.0.tgz", - "integrity": "sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.1.2.tgz", + "integrity": "sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13812,7 +14004,6 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -14333,7 +14524,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/data-urls": { @@ -15154,9 +15344,9 @@ } }, "node_modules/electron": { - "version": "33.2.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", - "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", + "version": "33.3.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.3.1.tgz", + "integrity": "sha512-Z7l2bVgpdKxHQMI4i0CirBX2n+iCYKOx5mbzNM3BpOyFELwlobEXKmzCmEnwP+3EcNeIhUQyIEBFQxN06QgdIw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -15395,9 +15585,9 @@ } }, "node_modules/electron/node_modules/@types/node": { - "version": "20.17.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", - "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", + "version": "20.17.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.12.tgz", + "integrity": "sha512-vo/wmBgMIiEA23A/knMfn/cf37VnuF52nZh5ZoW0GWt4e4sxNquibrMRJ7UQsA06+MBx9r/H1jsI9grYjQCQlw==", "dev": true, "license": "MIT", "dependencies": { @@ -15560,7 +15750,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -15809,7 +15998,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -17241,6 +17429,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -18899,7 +19093,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -18916,7 +19109,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -19216,7 +19408,6 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, "license": "MIT" }, "node_modules/is-bigint": { @@ -19292,7 +19483,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -22116,7 +22306,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -22459,34 +22648,31 @@ } }, "node_modules/lit": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz", - "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.2.1.tgz", + "integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==", "license": "BSD-3-Clause", "dependencies": { - "@lit/reactive-element": "^1.6.0", - "lit-element": "^3.3.0", - "lit-html": "^2.8.0" + "@lit/reactive-element": "^2.0.4", + "lit-element": "^4.1.0", + "lit-html": "^3.2.0" } }, "node_modules/lit-element": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz", - "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.1.1.tgz", + "integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==", "license": "BSD-3-Clause", "dependencies": { - "@lit-labs/ssr-dom-shim": "^1.1.0", - "@lit/reactive-element": "^1.3.0", - "lit-html": "^2.8.0" + "@lit-labs/ssr-dom-shim": "^1.2.0", + "@lit/reactive-element": "^2.0.4", + "lit-html": "^3.2.0" } }, "node_modules/lit-html": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz", - "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.2.1.tgz", + "integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==", "license": "BSD-3-Clause", "dependencies": { "@types/trusted-types": "^2.0.2" @@ -26075,7 +26261,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -26088,7 +26273,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -26107,7 +26291,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, "license": "MIT" }, "node_modules/parse-node-version": { @@ -26371,7 +26554,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -26408,7 +26590,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -27759,7 +27940,6 @@ "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true, "license": "MIT" }, "node_modules/regenerator-transform": { @@ -28058,7 +28238,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -28607,9 +28786,9 @@ } }, "node_modules/sass": { - "version": "1.81.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.81.0.tgz", - "integrity": "sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==", + "version": "1.83.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", + "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", "dev": true, "license": "MIT", "dependencies": { @@ -28669,9 +28848,9 @@ } }, "node_modules/sass/node_modules/chokidar": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", - "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", "dependencies": { @@ -29954,6 +30133,12 @@ "webpack": "^5.27.0" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -30016,7 +30201,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -30544,21 +30728,21 @@ } }, "node_modules/tldts": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.69.tgz", - "integrity": "sha512-Oh/CqRQ1NXNY7cy9NkTPUauOWiTro0jEYZTioGbOmcQh6EC45oribyIMJp0OJO3677r13tO6SKdWoGZUx2BDFw==", + "version": "6.1.71", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.71.tgz", + "integrity": "sha512-LQIHmHnuzfZgZWAf2HzL83TIIrD8NhhI0DVxqo9/FdOd4ilec+NTNZOlDZf7EwrTNoutccbsHjvWHYXLAtvxjw==", "license": "MIT", "dependencies": { - "tldts-core": "^6.1.69" + "tldts-core": "^6.1.71" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.69", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.69.tgz", - "integrity": "sha512-nygxy9n2PBUFQUtAXAc122gGo+04/j5qr5TGQFZTHafTKYvmARVXt2cA5rgero2/dnXUfkdPtiJoKmrd3T+wdA==", + "version": "6.1.71", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.71.tgz", + "integrity": "sha512-LRbChn2YRpic1KxY+ldL1pGXN/oVvKfCVufwfVzEQdFYNo39uF7AJa/WXdo+gYO7PTvdfkCPCed6Hkvz/kR7jg==", "license": "MIT" }, "node_modules/tmp": { @@ -30700,6 +30884,7 @@ "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=16" }, diff --git a/package.json b/package.json index 9922b6d7bac..641a49c4d04 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test:watch:all": "jest --watchAll", "test:types": "node ./scripts/test-types.js", "test:locales": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/test-locales.js", + "lint:dep-ownership": "tsc --project ./scripts/tsconfig.json && node ./scripts/dist/dep-ownership.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d . --disableRoutesGraph", "storybook": "ng run components:storybook", "build-storybook": "ng run components:build-storybook", @@ -42,7 +43,6 @@ "@angular-eslint/template-parser": "18.4.3", "@angular/cli": "18.2.12", "@angular/compiler-cli": "18.2.13", - "@angular/elements": "18.2.13", "@babel/core": "7.24.9", "@babel/preset-env": "7.24.8", "@compodoc/compodoc": "1.1.26", @@ -72,7 +72,7 @@ "@types/koa-json": "2.0.23", "@types/lowdb": "1.0.15", "@types/lunr": "2.3.7", - "@types/node": "22.10.2", + "@types/node": "22.10.5", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", "@types/node-ipc": "9.2.3", @@ -80,8 +80,8 @@ "@types/proper-lockfile": "4.1.4", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.5", - "@typescript-eslint/eslint-plugin": "7.16.1", - "@typescript-eslint/parser": "7.16.1", + "@typescript-eslint/eslint-plugin": "8.19.1", + "@typescript-eslint/parser": "8.19.1", "@webcomponents/custom-elements": "1.6.0", "@yao-pkg/pkg": "5.16.1", "autoprefixer": "10.4.20", @@ -89,11 +89,11 @@ "base64-loader": "1.0.0", "browserslist": "4.23.2", "chromatic": "11.20.2", - "concurrently": "9.1.0", + "concurrently": "9.1.2", "copy-webpack-plugin": "12.0.2", "cross-env": "7.0.3", "css-loader": "7.1.2", - "electron": "33.2.1", + "electron": "33.3.1", "electron-builder": "24.13.3", "electron-log": "5.2.4", "electron-reload": "2.0.0-alpha.1", @@ -125,7 +125,7 @@ "process": "0.11.10", "remark-gfm": "4.0.0", "rimraf": "6.0.1", - "sass": "1.81.0", + "sass": "1.83.1", "sass-loader": "16.0.4", "storybook": "8.4.7", "style-loader": "4.0.0", @@ -156,6 +156,7 @@ "@angular/router": "18.2.13", "@bitwarden/sdk-internal": "0.2.0-main.38", "@electron/fuses": "1.8.0", + "@emotion/css": "11.13.5", "@koa/multer": "3.0.2", "@koa/router": "13.1.0", "@microsoft/signalr": "8.0.7", @@ -167,7 +168,7 @@ "bootstrap": "4.6.0", "braintree-web-drop-in": "1.43.0", "buffer": "6.0.3", - "bufferutil": "4.0.8", + "bufferutil": "4.0.9", "chalk": "4.1.2", "commander": "11.1.0", "core-js": "3.39.0", @@ -180,6 +181,7 @@ "koa": "2.15.3", "koa-bodyparser": "4.4.1", "koa-json": "2.0.2", + "lit": "3.2.1", "lowdb": "1.0.0", "lunr": "2.3.9", "multer": "1.4.5-lts.1", @@ -198,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.69", + "tldts": "6.1.71", "utf-8-validate": "6.0.5", "zone.js": "0.14.10", "zxcvbn": "4.4.2" diff --git a/scripts/dep-ownership.ts b/scripts/dep-ownership.ts new file mode 100644 index 00000000000..e574a3e9e96 --- /dev/null +++ b/scripts/dep-ownership.ts @@ -0,0 +1,31 @@ +/* eslint-disable no-console */ + +/// Ensure that all dependencies in package.json have an owner in the renovate.json file. + +import fs from "fs"; +import path from "path"; + +const renovateConfig = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", ".github", "renovate.json"), "utf8"), +); + +const packagesWithOwners = renovateConfig.packageRules + .flatMap((rule: any) => rule.matchPackageNames) + .filter((packageName: string) => packageName != null); + +const packageJson = JSON.parse( + fs.readFileSync(path.join(__dirname, "..", "..", "package.json"), "utf8"), +); +const dependencies = Object.keys(packageJson.dependencies).concat( + Object.keys(packageJson.devDependencies), +); + +const missingOwners = dependencies.filter((dep) => !packagesWithOwners.includes(dep)); + +if (missingOwners.length > 0) { + console.error("Missing owners for the following dependencies:"); + console.error(missingOwners.join("\n")); + process.exit(1); +} + +console.log("All dependencies have owners."); diff --git a/scripts/test-types.js b/scripts/test-types.js index 2155406cdf0..9534558af30 100644 --- a/scripts/test-types.js +++ b/scripts/test-types.js @@ -16,10 +16,15 @@ function getFiles(dir) { return results; } -const files = getFiles(path.join(__dirname, "..", "libs")).filter((file) => { - const name = path.basename(file); - return name === "tsconfig.spec.json"; -}); +const files = getFiles(path.join(__dirname, "..", "libs")) + .filter((file) => { + const name = path.basename(file); + return name === "tsconfig.spec.json"; + }) + .filter((path) => { + // Exclude shared since it's not actually a library + return !path.includes("libs/shared/"); + }); concurrently([ { diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 80f70e47b09..941a612a30c 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -14,6 +14,7 @@ "declarationDir": "dist/types", "outDir": "dist", "baseUrl": ".", + "allowJs": true, "paths": { "@bitwarden/admin-console": ["./libs/admin-console/src"], "@bitwarden/angular/*": ["./libs/angular/src/*"], diff --git a/tsconfig.json b/tsconfig.json index 47b561e0829..91b4ee7dd6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,6 +33,7 @@ "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/key-management": ["./libs/key-management/src"], + "@bitwarden/key-management/angular": ["./libs/key-management/src/angular"], "@bitwarden/platform": ["./libs/platform/src"], "@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"], "@bitwarden/tools-card": ["./libs/tools/card/src"],