diff --git a/.eslintrc.json b/.eslintrc.json index 671e7b2fab..61bebbf483 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -246,6 +246,22 @@ } ] } + }, + { + "files": ["**/*.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/**" + ] + } + ] + } } ] } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e9c1f229a5..aba7d4e0e0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,7 @@ libs/common/src/admin-console @bitwarden/team-admin-console-dev libs/admin-console @bitwarden/team-admin-console-dev ## Billing team files ## +apps/browser/src/billing @bitwarden/team-billing-dev apps/web/src/app/billing @bitwarden/team-billing-dev libs/angular/src/billing @bitwarden/team-billing-dev libs/common/src/billing @bitwarden/team-billing-dev diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml index 23f4bd35f1..f924c5c98e 100644 --- a/.github/workflows/build-browser.yml +++ b/.github/workflows/build-browser.yml @@ -164,6 +164,10 @@ jobs: run: npm run dist:mv3 working-directory: browser-source/apps/browser + - name: Build Chrome Manifest v3 Beta + run: npm run dist:chrome:beta + working-directory: browser-source/apps/browser + - name: Gulp run: gulp ci working-directory: browser-source/apps/browser @@ -196,6 +200,13 @@ jobs: path: browser-source/apps/browser/dist/dist-chrome-mv3.zip if-no-files-found: error + - name: Upload Chrome MV3 Beta artifact (DO NOT USE FOR PROD) + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 + with: + name: DO-NOT-USE-FOR-PROD-dist-chrome-MV3-beta-${{ env._BUILD_NUMBER }}.zip + path: browser-source/apps/browser/dist/dist-chrome-mv3-beta.zip + if-no-files-found: error + - name: Upload Firefox artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 769e700588..b034136f58 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -128,29 +128,90 @@ jobs: - name: Success Code run: exit 0 - get-branch-or-tag-sha: - name: Get Branch or Tag SHA + artifact-check: + name: Check if Web artifact is present runs-on: ubuntu-22.04 + needs: setup + env: + _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} outputs: - branch-or-tag-sha: ${{ steps.get-branch-or-tag-sha.outputs.sha }} + artifact-build-commit: ${{ steps.set-artifact-commit.outputs.commit }} steps: - - name: Checkout Branch - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' + if: ${{ inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts-run-id + continue-on-error: true with: - ref: ${{ inputs.branch-or-tag }} - fetch-depth: 0 + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + run_id: ${{ inputs.build-web-run-id }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - name: Get Branch or Tag SHA - id: get-branch-or-tag-sha + - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' + if: ${{ !inputs.build-web-run-id }} + uses: bitwarden/gh-actions/download-artifacts@main + id: download-latest-artifacts + continue-on-error: true + with: + workflow: build-web.yml + path: apps/web + workflow_conclusion: success + branch: ${{ inputs.branch-or-tag }} + artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} + + - name: Login to Azure + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 + with: + creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} + + - name: Retrieve secrets for Build trigger + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + id: retrieve-secret + uses: bitwarden/gh-actions/get-keyvault-secrets@main + with: + keyvault: "bitwarden-ci" + secrets: "github-pat-bitwarden-devops-bot-repo-scope" + + - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' + if: ${{ steps.download-latest-artifacts.outcome == 'failure' }} + uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 + id: trigger-build-web + with: + owner: bitwarden + repo: clients + github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} + workflow_file_name: build-web.yml + ref: ${{ inputs.branch-or-tag }} + wait_interval: 100 + + - name: Set artifact build commit + id: set-artifact-commit + env: + GH_TOKEN: ${{ github.token }} run: | - echo "sha=$(git rev-parse origin/${{ inputs.branch-or-tag }})" >> $GITHUB_OUTPUT + # 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 + + 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. + commit=$(gh api /repos/bitwarden/clients/actions/runs/${{ steps.trigger-build-web.outputs.workflow_id }}/artifacts --jq '.artifacts[0].workflow_run.head_sha') + echo "commit=$commit" >> $GITHUB_OUTPUT + + else + # Set the commit to the output of step download-latest-artifacts. + echo "commit=${{ steps.download-latest-artifacts.outputs.artifact-build-commit }}" >> $GITHUB_OUTPUT + fi notify-start: name: Notify Slack with start message needs: - approval - setup - - get-branch-or-tag-sha + - artifact-check runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} outputs: @@ -165,65 +226,20 @@ jobs: tag: ${{ inputs.branch-or-tag }} slack-channel: team-eng-qa-devops event: 'start' - commit-sha: ${{ needs.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + 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 }} - artifact-check: - name: Check if Web artifact is present + update-summary: + name: Display commit + needs: artifact-check runs-on: ubuntu-22.04 - needs: setup - env: - _ENVIRONMENT_ARTIFACT: ${{ needs.setup.outputs.environment-artifact }} steps: - - name: 'Download latest cloud asset using GitHub Run ID: ${{ inputs.build-web-run-id }}' - if: ${{ inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-latest-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - run_id: ${{ inputs.build-web-run-id }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: 'Download latest cloud asset from branch/tag: ${{ inputs.branch-or-tag }}' - if: ${{ !inputs.build-web-run-id }} - uses: bitwarden/gh-actions/download-artifacts@main - id: download-artifacts - continue-on-error: true - with: - workflow: build-web.yml - path: apps/web - workflow_conclusion: success - branch: ${{ inputs.branch-or-tag }} - artifacts: ${{ env._ENVIRONMENT_ARTIFACT }} - - - name: Login to Azure - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets for Build trigger - if: ${{ steps.download-artifacts.outcome == 'failure' }} - id: retrieve-secret - uses: bitwarden/gh-actions/get-keyvault-secrets@main - with: - keyvault: "bitwarden-ci" - secrets: "github-pat-bitwarden-devops-bot-repo-scope" - - - name: 'Trigger build web for missing branch/tag ${{ inputs.branch-or-tag }}' - if: ${{ steps.download-artifacts.outcome == 'failure' }} - uses: convictional/trigger-workflow-and-wait@f69fa9eedd3c62a599220f4d5745230e237904be # v1.6.5 - with: - owner: bitwarden - repo: clients - github_token: ${{ steps.retrieve-secret.outputs.github-pat-bitwarden-devops-bot-repo-scope }} - workflow_file_name: build-web.yml - ref: ${{ inputs.branch-or-tag }} - wait_interval: 100 + - name: Display commit SHA + run: | + REPO_URL="https://github.com/bitwarden/clients/commit" + COMMIT_SHA="${{ needs.artifact-check.outputs.artifact-build-commit }}" + echo ":steam_locomotive: View [commit]($REPO_URL/$COMMIT_SHA)" >> $GITHUB_STEP_SUMMARY azure-deploy: name: Deploy Web Vault to ${{ inputs.environment }} Storage Account @@ -248,6 +264,7 @@ jobs: environment: ${{ env._ENVIRONMENT_NAME }} task: 'deploy' description: 'Deployment from branch/tag: ${{ inputs.branch-or-tag }}' + ref: ${{ needs.artifact-check.outputs.artifact-build-commit }} - name: Login to Azure uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 @@ -349,10 +366,10 @@ jobs: runs-on: ubuntu-22.04 if: ${{ always() && contains( inputs.environment , 'QA' ) }} needs: + - setup - notify-start - azure-deploy - - setup - - get-branch-or-tag-sha + - artifact-check steps: - uses: bitwarden/gh-actions/report-deployment-status-to-slack@main with: @@ -362,6 +379,6 @@ jobs: 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.get-branch-or-tag-sha.outputs.branch-or-tag-sha }} + 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/release-desktop-beta.yml b/.github/workflows/release-desktop-beta.yml index b9e2d7a8c8..46f4ffad57 100644 --- a/.github/workflows/release-desktop-beta.yml +++ b/.github/workflows/release-desktop-beta.yml @@ -955,11 +955,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 @@ -985,20 +981,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Update deployment status to Success if: ${{ success() }} uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3 diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index cf857d7177..dc6957d00d 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -115,11 +115,7 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - name: Download all artifacts if: ${{ github.event.inputs.release_type != 'Dry Run' }} @@ -169,21 +165,6 @@ jobs: --recursive \ --quiet - - name: Publish artifacts to R2 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - working-directory: apps/desktop/artifacts - run: | - aws s3 cp ./ $AWS_S3_BUCKET_NAME/desktop/ \ - --recursive \ - --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - name: Get checksum files uses: bitwarden/gh-actions/get-checksum@main with: diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index a5b5fc69b1..a6ca2f1e31 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -31,29 +31,21 @@ jobs: keyvault: "bitwarden-ci" secrets: "aws-electron-access-id, aws-electron-access-key, - aws-electron-bucket-name, - r2-electron-access-id, - r2-electron-access-key, - r2-electron-bucket-name, - cf-prod-account" + aws-electron-bucket-name" - - name: Download channel update info files from R2 + - name: Download channel update info files from S3 env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} + AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} + AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} + AWS_DEFAULT_REGION: 'us-west-2' + AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \ --quiet \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - name: Check new rollout percentage env: @@ -95,20 +87,3 @@ jobs: aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ --acl "public-read" - - - name: Publish channel update info files to R2 - env: - AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.r2-electron-access-id }} - AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.r2-electron-access-key }} - AWS_DEFAULT_REGION: 'us-east-1' - AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.r2-electron-bucket-name }} - CF_ACCOUNT: ${{ steps.retrieve-secrets.outputs.cf-prod-account }} - run: | - aws s3 cp latest.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-linux.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com - - aws s3 cp latest-mac.yml $AWS_S3_BUCKET_NAME/desktop/ \ - --endpoint-url https://${CF_ACCOUNT}.r2.cloudflarestorage.com diff --git a/.storybook/main.ts b/.storybook/main.ts index c71a74c2a7..26eee201f9 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -4,11 +4,14 @@ import remarkGfm from "remark-gfm"; const config: StorybookConfig = { stories: [ + "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", "../apps/web/src/**/*.mdx", "../apps/web/src/**/*.stories.@(js|jsx|ts|tsx)", + "../apps/browser/src/**/*.mdx", + "../apps/browser/src/**/*.stories.@(js|jsx|ts|tsx)", "../bitwarden_license/bit-web/src/**/*.mdx", "../bitwarden_license/bit-web/src/**/*.stories.@(js|jsx|ts|tsx)", ], diff --git a/.storybook/tsconfig.json b/.storybook/tsconfig.json index 113cc5bcde..34acc9a740 100644 --- a/.storybook/tsconfig.json +++ b/.storybook/tsconfig.json @@ -1,12 +1,10 @@ { "extends": "../tsconfig", "compilerOptions": { - "types": ["node", "jest", "chrome"], "allowSyntheticDefaultImports": true }, - "exclude": ["../src/test.setup.ts", "../apps/src/**/*.spec.ts", "../libs/**/*.spec.ts"], + "exclude": ["../src/test.setup.ts", "../apps/**/*.spec.ts", "../libs/**/*.spec.ts"], "files": [ - "./typings.d.ts", "./preview.tsx", "../libs/components/src/main.ts", "../libs/components/src/polyfills.ts" diff --git a/.storybook/typings.d.ts b/.storybook/typings.d.ts deleted file mode 100644 index c94d67b1a2..0000000000 --- a/.storybook/typings.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.md" { - const content: string; - export default content; -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 27e3a9b293..3a70af3481 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,6 @@ "**/locales/*[^n]/messages.json": true, "**/_locales/[^e]*/messages.json": true, "**/_locales/*[^n]/messages.json": true - } + }, + "rust-analyzer.linkedProjects": ["apps/desktop/desktop_native/Cargo.toml"] } diff --git a/angular.json b/angular.json index 4b62c771cb..cdf213e39d 100644 --- a/angular.json +++ b/angular.json @@ -142,7 +142,15 @@ "configDir": ".storybook", "browserTarget": "components:build", "compodoc": true, - "compodocArgs": ["-p", "./tsconfig.json", "-e", "json", "-d", "."], + "compodocArgs": [ + "-p", + "./tsconfig.json", + "-e", + "json", + "-d", + ".", + "--disableRoutesGraph" + ], "port": 6006 } }, diff --git a/apps/browser/config/base.json b/apps/browser/config/base.json index 8a3ccc14d3..6c428c43d2 100644 --- a/apps/browser/config/base.json +++ b/apps/browser/config/base.json @@ -1,5 +1,5 @@ { - "dev_flags": {}, + "devFlags": {}, "flags": { "showPasswordless": true, "enableCipherKeyEncryption": false, diff --git a/apps/browser/config/development.json b/apps/browser/config/development.json index 1b628c173c..e0925ebecc 100644 --- a/apps/browser/config/development.json +++ b/apps/browser/config/development.json @@ -1,9 +1,9 @@ { "devFlags": { - "storeSessionDecrypted": false, "managedEnvironment": { "base": "https://localhost:8080" - } + }, + "skipWelcomeOnInstall": true }, "flags": { "showPasswordless": true, diff --git a/apps/browser/gulpfile.js b/apps/browser/gulpfile.js index 6a0980fc27..3fe2c44dd1 100644 --- a/apps/browser/gulpfile.js +++ b/apps/browser/gulpfile.js @@ -30,11 +30,27 @@ const filters = { safari: ["!build/safari/**/*"], }; +/** + * Converts a number to a tuple containing two Uint16's + * @param num {number} This number is expected to be a integer style number with no decimals + * + * @returns {number[]} A tuple containing two elements that are both numbers. + */ +function numToUint16s(num) { + var arr = new ArrayBuffer(4); + var view = new DataView(arr); + view.setUint32(0, num, false); + return [view.getUint16(0), view.getUint16(2)]; +} + function buildString() { var build = ""; if (process.env.MANIFEST_VERSION) { build = `-mv${process.env.MANIFEST_VERSION}`; } + if (process.env.BETA_BUILD === "1") { + build += "-beta"; + } if (process.env.BUILD_NUMBER && process.env.BUILD_NUMBER !== "") { build = `-${process.env.BUILD_NUMBER}`; } @@ -65,6 +81,9 @@ function distFirefox() { manifest.optional_permissions = manifest.optional_permissions.filter( (permission) => permission !== "privacy", ); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -72,6 +91,9 @@ function distFirefox() { function distOpera() { return dist("opera", (manifest) => { delete manifest.applications; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -81,6 +103,9 @@ function distChrome() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -90,6 +115,9 @@ function distEdge() { delete manifest.applications; delete manifest.sidebar_action; delete manifest.commands._execute_sidebar_action; + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }); } @@ -210,6 +238,9 @@ async function safariCopyBuild(source, dest) { delete manifest.commands._execute_sidebar_action; delete manifest.optional_permissions; manifest.permissions.push("nativeMessaging"); + if (process.env.BETA_BUILD === "1") { + manifest = applyBetaLabels(manifest); + } return manifest; }), ), @@ -235,6 +266,30 @@ async function ciCoverage(cb) { .pipe(gulp.dest(paths.coverage)); } +function applyBetaLabels(manifest) { + manifest.name = "Bitwarden Password Manager BETA"; + manifest.short_name = "Bitwarden BETA"; + manifest.description = "THIS EXTENSION IS FOR BETA TESTING BITWARDEN."; + if (process.env.GITHUB_RUN_ID) { + const existingVersionParts = manifest.version.split("."); // 3 parts expected 2024.4.0 + + // GITHUB_RUN_ID is a number like: 8853654662 + // which will convert to [ 4024, 3206 ] + // and a single incremented id of 8853654663 will become [ 4024, 3207 ] + const runIdParts = numToUint16s(parseInt(process.env.GITHUB_RUN_ID)); + + // Only use the first 2 parts from the given version number and base the other 2 numbers from the GITHUB_RUN_ID + // Example: 2024.4.4024.3206 + const betaVersion = `${existingVersionParts[0]}.${existingVersionParts[1]}.${runIdParts[0]}.${runIdParts[1]}`; + + manifest.version_name = `${betaVersion} beta - ${process.env.GITHUB_SHA.slice(0, 8)}`; + manifest.version = betaVersion; + } else { + manifest.version = `${manifest.version}.0`; + } + return manifest; +} + exports["dist:firefox"] = distFirefox; exports["dist:chrome"] = distChrome; exports["dist:opera"] = distOpera; diff --git a/apps/browser/package.json b/apps/browser/package.json index ee6d100572..278a3b6c52 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,16 +1,20 @@ { "name": "@bitwarden/browser", - "version": "2024.4.1", + "version": "2024.5.0", "scripts": { "build": "webpack", "build:mv3": "cross-env MANIFEST_VERSION=3 webpack", "build:watch": "webpack --watch", "build:watch:mv3": "cross-env MANIFEST_VERSION=3 webpack --watch", "build:prod": "cross-env NODE_ENV=production webpack", + "build:prod:beta": "cross-env BETA_BUILD=1 NODE_ENV=production webpack", "build:prod:watch": "cross-env NODE_ENV=production webpack --watch", "dist": "npm run build:prod && gulp dist", + "dist:beta": "npm run build:prod:beta && cross-env BETA_BUILD=1 gulp dist", "dist:mv3": "cross-env MANIFEST_VERSION=3 npm run build:prod && cross-env MANIFEST_VERSION=3 gulp dist", + "dist:mv3:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist", "dist:chrome": "npm run build:prod && gulp dist:chrome", + "dist:chrome:beta": "cross-env MANIFEST_VERSION=3 npm run build:prod:beta && cross-env MANIFEST_VERSION=3 BETA_BUILD=1 gulp dist:chrome", "dist:firefox": "npm run build:prod && gulp dist:firefox", "dist:opera": "npm run build:prod && gulp dist:opera", "dist:safari": "npm run build:prod && gulp dist:safari", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 1f7c5bbe98..6c83d771e9 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدير كلمات مرور مجاني", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "مدير كلمات مرور مجاني وآمن لجميع أجهزتك.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "قم بالتسجيل أو إنشاء حساب جديد للوصول إلى خزنتك الآمنة." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغيير كلمة المرور الرئيسية" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "أُضيف المجلد" }, - "changeMasterPass": { - "message": "تغيير كلمة المرور الرئيسية" - }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" - }, "twoStepLoginConfirmation": { "message": "تسجيل الدخول بخطوتين يجعل حسابك أكثر أمنا من خلال مطالبتك بالتحقق من تسجيل الدخول باستخدام جهاز آخر مثل مفتاح الأمان، تطبيق المصادقة، الرسائل القصيرة، المكالمة الهاتفية، أو البريد الإلكتروني. يمكن تمكين تسجيل الدخول بخطوتين على خزنة الويب bitwarden.com. هل تريد زيارة الموقع الآن؟" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "قفل المخزن" }, - "privateModeWarning": { - "message": "دعم الوضع الخاص تجريبي وبعض الميزات محدودة." - }, "customFields": { "message": "الحقول المخصصة" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 2111ea6704..18fc6acca8 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ödənişsiz Parol Meneceri", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvənli anbarınıza müraciət etmək üçün giriş edin və ya yeni bir hesab yaradın." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolu dəyişdir" }, + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." + }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Qovluq əlavə edildi" }, - "changeMasterPass": { - "message": "Ana parolu dəyişdir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" - }, "twoStepLoginConfirmation": { "message": "İki addımlı giriş, güvənlik açarı, kimlik doğrulayıcı tətbiq, SMS, telefon zəngi və ya e-poçt kimi digər cihazlarla girişinizi doğrulamanızı tələb edərək hesabınızı daha da güvənli edir. İki addımlı giriş, bitwarden.com veb anbarında qurula bilər. Veb saytı indi ziyarət etmək istəyirsiniz?" }, @@ -1045,7 +1045,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -1072,7 +1072,7 @@ "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { - "message": "Avto-doldurma nişanı seçiləndə", + "message": "Avto-doldurma ikonu seçiləndə", "description": "Overlay appearance select option for showing the field on click of the overlay icon" }, "enableAutoFillOnPageLoad": { @@ -1109,7 +1109,7 @@ "message": "Anbarı açılan pəncərədə aç" }, "commandOpenSidebar": { - "message": "Anbar yan sətirdə aç" + "message": "Anbarı yan çubuqda aç" }, "commandAutofillDesc": { "message": "Hazırkı veb sayt üçün son istifadə edilən giriş məlumatlarını avto-doldur" @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Anbarı kilidlə" }, - "privateModeWarning": { - "message": "Gizli rejim dəstəyi təcrübidir və bəzi özəlliklər limitlidir." - }, "customFields": { "message": "Özəl sahələr" }, @@ -1162,7 +1159,7 @@ "message": "Bu brauzer bu açılan pəncərədə U2F tələblərini emal edə bilmir. U2F istifadə edərək giriş etmək üçün bu açılan pəncərəni yeni bir pəncərədə açmaq istəyirsiniz?" }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -1724,7 +1721,7 @@ "message": "İcazə tələb xətası" }, "nativeMessaginPermissionSidebarDesc": { - "message": "Bu əməliyyatı kənar çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." + "message": "Bu əməliyyat yan çubuqda icra edilə bilməz. Lütfən açılan pəncərədə yenidən sınayın." }, "personalOwnershipSubmitError": { "message": "Müəssisə Siyasətinə görə, elementləri şəxsi anbarınızda saxlamağınız məhdudlaşdırılıb. Sahiblik seçimini təşkilat olaraq dəyişdirin və mövcud kolleksiyalar arasından seçim edin." @@ -1924,10 +1921,10 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendLinuxChromiumFileWarning": { - "message": "Bir fayl seçmək üçün (mümkünsə) kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Bir fayl seçmək üçün (mümkünsə) yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendFirefoxFileWarning": { - "message": "Firefox istifadə edərək bir fayl seçmək üçün kənar çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." + "message": "Firefox istifadə edərək bir fayl seçmək üçün yan çubuqdakı uzantını açın və ya bu bannerə klikləyərək yeni bir pəncərədə açın." }, "sendSafariFileWarning": { "message": "Safari istifadə edərək bir fayl seçmək üçün bu bannerə klikləyərək yeni bir pəncərədə açın." @@ -3000,16 +2997,36 @@ "message": "Kimlik məlumatlarını saxlama xətası. Detallar üçün konsolu yoxlayın.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Uğurlu" + }, "removePasskey": { "message": "Parolu sil" }, "passkeyRemoved": { "message": "Parol silindi" }, - "unassignedItemsBanner": { - "message": "Bildiriş: Təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyəndir və yalnız Admin Konsolu vasitəsilə əlçatandır. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." }, - "unassignedItemsBannerSelfHost": { - "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "bir kolleksiyaya təyin edin.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Konsolu" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 08cb351abb..44dc85d2b9 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – бясплатны менеджар пароляў", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Увайдзіце або стварыце новы ўліковы запіс для доступу да бяспечнага сховішча." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змяніць асноўны пароль" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка дададзена" }, - "changeMasterPass": { - "message": "Змяніць асноўны пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапны ўваход робіць ваш уліковы запіс больш бяспечным, патрабуючы пацвярджэнне ўваходу на іншай прыладзе з выкарыстаннем ключа бяспекі, праграмы аўтэнтыфікацыі, SMS, тэлефоннага званка або электроннай пошты. Двухэтапны ўваход уключаецца на bitwarden.com. Перайсці на вэб-сайт, каб зрабіць гэта?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблакіраваць сховішча" }, - "privateModeWarning": { - "message": "Прыватны рэжым - гэта эксперыментальная функцыя і некаторыя магчымасці ў ім абмежаваны." - }, "customFields": { "message": "Карыстальніцкія палі" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 87dfc8d3be..cb12459a44 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -3,12 +3,12 @@ "message": "Битуорден (Bitwarden)" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Безопасно и безплатно управление за всичките ви устройства.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Впишете се или създайте нов абонамент, за да достъпите защитен трезор." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промяна на главната парола" }, + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." + }, "fingerprintPhrase": { "message": "Уникална фраза", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Добавена папка" }, - "changeMasterPass": { - "message": "Промяна на главната парола" - }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" - }, "twoStepLoginConfirmation": { "message": "Двустепенното вписване защитава регистрацията ви, като ви кара да потвърдите влизането си чрез устройство-ключ, приложение за удостоверение, мобилно съобщение, телефонно обаждане или електронна поща. Двустепенното вписване може да се включи чрез сайта bitwarden.com. Искате ли да го посетите?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заключване на трезора" }, - "privateModeWarning": { - "message": "Поддръжката на частния режим е експериментална и някои функционалности са ограничени." - }, "customFields": { "message": "Допълнителни полета" }, @@ -3000,16 +2997,36 @@ "message": "Грешка при запазването на идентификационните данни. Вижте конзолата за подробности.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успех" + }, "removePasskey": { "message": "Премахване на секретния ключ" }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а са достъпни само през Административната конзола." }, - "unassignedItemsBannerSelfHost": { - "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "за да ги направите видими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Административна конзола" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 1bdaeef7c6..8156e3c6f1 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক।", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "আপনার সুরক্ষিত ভল্টে প্রবেশ করতে লগ ইন করুন অথবা একটি নতুন অ্যাকাউন্ট তৈরি করুন।" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ফোল্ডার জোড়া হয়েছে" }, - "changeMasterPass": { - "message": "মূল পাসওয়ার্ড পরিবর্তন" - }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" - }, "twoStepLoginConfirmation": { "message": "দ্বি-পদক্ষেপ লগইন অন্য ডিভাইসে আপনার লগইনটি যাচাই করার জন্য সিকিউরিটি কী, প্রমাণীকরণকারী অ্যাপ্লিকেশন, এসএমএস, ফোন কল বা ই-মেইল ব্যাবহারের মাধ্যমে আপনার অ্যাকাউন্টকে আরও সুরক্ষিত করে। bitwarden.com ওয়েব ভল্টে দ্বি-পদক্ষেপের লগইন সক্ষম করা যাবে। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ভল্ট লক করুন" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "পছন্দসই ক্ষেত্র" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index 92a667afeb..a7334319b5 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ili napravite novi račun da biste pristupili svom sigurnom trezoru." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index fc67602b60..d9cd14102e 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden - Gestor de contrasenyes", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius.", - "description": "Extension description" + "message": "A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicieu sessió o creeu un compte nou per accedir a la caixa forta." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Canvia la contrasenya mestra" }, + "continueToWebApp": { + "message": "Continua cap a l'aplicació web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Podeu canviar la vostra contrasenya mestra a l'aplicació web de Bitwarden." + }, "fingerprintPhrase": { "message": "Frase d'empremta digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta afegida" }, - "changeMasterPass": { - "message": "Canvia la contrasenya mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" - }, "twoStepLoginConfirmation": { "message": "L'inici de sessió en dues passes fa que el vostre compte siga més segur, ja que obliga a verificar el vostre inici de sessió amb un altre dispositiu, com ara una clau de seguretat, una aplicació autenticadora, un SMS, una trucada telefònica o un correu electrònic. Es pot habilitar l'inici de sessió en dues passes a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Tanca la caixa forta" }, - "privateModeWarning": { - "message": "El suport del mode privat és experimental i algunes funcions són limitades." - }, "customFields": { "message": "Camps personalitzats" }, @@ -3000,16 +2997,36 @@ "message": "S'ha produït un error en guardar les credencials. Consulteu la consola per obtenir més informació.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Èxit" + }, "removePasskey": { "message": "Suprimeix la clau de pas" }, "passkeyRemoved": { "message": "Clau de pas suprimida" }, - "unassignedItemsBanner": { - "message": "Nota: els elements de l'organització sense assignar ja no es veuran a la vista \"Totes les caixes fortes\" i només es veuran des de la consola d'administració. Assigneu-los-hi una col·lecció des de la consola per fer-los visibles." + "unassignedItemsBannerNotice": { + "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes i només es poden accedir des de la Consola d'administració." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes i només es podran accedir des de la Consola d'administració." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assigna aquests elements a una col·lecció de", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per fer-los visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Consola d'administració" + }, + "errorAssigningTargetCollection": { + "message": "S'ha produït un error en assignar la col·lecció de destinació." + }, + "errorAssigningTargetFolder": { + "message": "S'ha produït un error en assignar la carpeta de destinació." } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index d989d25bf2..058378ff17 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Bezplatný správce hesel", + "message": "Bitwarden - Správce hesel", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení.", - "description": "Extension description" + "message": "Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Pro přístup do Vašeho bezpečného trezoru se přihlaste nebo si vytvořte nový účet." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Změnit hlavní heslo" }, + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." + }, "fingerprintPhrase": { "message": "Fráze otisku prstu", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Složka byla přidána" }, - "changeMasterPass": { - "message": "Změnit hlavní heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" - }, "twoStepLoginConfirmation": { "message": "Dvoufázové přihlášení činí Váš účet mnohem bezpečnějším díky nutnosti po každém úspěšném přihlášení zadat ověřovací kód získaný z bezpečnostního klíče, aplikace, SMS, telefonního hovoru nebo e-mailu. Dvoufázové přihlášení lze aktivovat na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zamkne trezor." }, - "privateModeWarning": { - "message": "Podpora soukromého režimu je experimentální a některé funkce jsou omezené." - }, "customFields": { "message": "Vlastní pole" }, @@ -3000,16 +2997,36 @@ "message": "Chyba při ukládání přihlašovacích údajů. Podrobnosti naleznete v konzoli.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Úspěch" + }, "removePasskey": { "message": "Odebrat přístupový klíč" }, "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" }, - "unassignedItemsBanner": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů a jsou nyní přístupné jen v konzoli správce. Přiřaďte tyto položky do kolekce z konzole pro správce, aby byly viditelné." + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve vašem zobrazení všech trezorů a jsou nyní přístupné pouze v konzoli správce." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve vašem zobrazení všech trezorů a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "aby byly viditelné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konzole správce" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 79178bc9d5..5ec7b9f483 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Rheolydd cyfineiriau am ddim", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Rheolydd cyfrineiriau diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Mewngofnodwch neu crëwch gyfrif newydd i gael mynediad i'ch cell ddiogel." @@ -23,7 +23,7 @@ "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Canslo" }, "close": { "message": "Cau" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Newid y prif gyfrinair" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Ymadrodd unigryw", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -312,7 +318,7 @@ "message": "Golygu" }, "view": { - "message": "View" + "message": "Gweld" }, "noItemsInList": { "message": "Does dim eitemau i'w rhestru." @@ -543,10 +549,10 @@ "message": "Ydych chi'n siŵr eich bod am allgofnodi?" }, "yes": { - "message": "Yes" + "message": "Ydw" }, "no": { - "message": "No" + "message": "Na" }, "unexpectedError": { "message": "An unexpected error has occurred." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ffolder wedi'i hychwanegu" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Cloi'r gell" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Meysydd addasedig" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index d808d97412..4c90522e1e 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis adgangskodemanager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "En sikker og gratis adgangskodemanager til alle dine enheder.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log ind eller opret en ny konto for at få adgang til din sikre boks." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Skift hovedadgangskode" }, + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." + }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mappe tilføjet" }, - "changeMasterPass": { - "message": "Skift hovedadgangskode" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ændre din hovedadgangskode i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" - }, "twoStepLoginConfirmation": { "message": "To-trins login gør din konto mere sikker ved at kræve, at du verificerer dit login med en anden enhed, såsom en sikkerhedsnøgle, autentificeringsapp, SMS, telefonopkald eller e-mail. To-trins login kan aktiveres i bitwarden.com web-boksen. Vil du besøge hjemmesiden nu?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås boksen" }, - "privateModeWarning": { - "message": "Understøttelse af privat tilstand er eksperimentel, og nogle funktioner er begrænsede." - }, "customFields": { "message": "Brugerdefinerede felter" }, @@ -3000,16 +2997,36 @@ "message": "Fejl under import. Tjek konsollen for detaljer.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Gennemført" + }, "removePasskey": { "message": "Fjern adgangsnøgle" }, "passkeyRemoved": { "message": "Adgangsnøgle fjernet" }, - "unassignedItemsBanner": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen og er kun tilgængelige via Adminkonsollen. Føj disse emner til en samling fra Adminkonsollen for at gøre dem synlige." + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." }, - "unassignedItemsBannerSelfHost": { - "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "for at gøre dem synlige.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin-konsol" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index deb92e992d..35100a77d2 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Kostenloser Passwortmanager", + "message": "Bitwarden Passwortmanager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ein sicherer und kostenloser Passwortmanager für all deine Geräte.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Melde dich an oder erstelle ein neues Konto, um auf deinen Tresor zuzugreifen." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Master-Passwort ändern" }, + "continueToWebApp": { + "message": "Weiter zur Web-App?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." + }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Ordner hinzugefügt" }, - "changeMasterPass": { - "message": "Master-Passwort ändern" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im Bitwarden.com Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" - }, "twoStepLoginConfirmation": { "message": "Mit der Zwei-Faktor-Authentifizierung wird dein Konto zusätzlich abgesichert, da jede Anmeldung mit einem anderen Gerät wie einem Sicherheitsschlüssel, einer Authentifizierungs-App, einer SMS, einem Anruf oder einer E-Mail verifiziert werden muss. Die Zwei-Faktor-Authentifizierung kann im bitwarden.com Web-Tresor aktiviert werden. Möchtest du die Website jetzt öffnen?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Den Tresor sperren" }, - "privateModeWarning": { - "message": "Die Unterstützung des privaten Modus ist experimentell und einige Funktionen sind eingeschränkt." - }, "customFields": { "message": "Benutzerdefinierte Felder" }, @@ -3000,16 +2997,36 @@ "message": "Fehler beim Speichern der Zugangsdaten. Details in der Konsole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Erfolg" + }, "removePasskey": { "message": "Passkey entfernen" }, "passkeyRemoved": { "message": "Passkey entfernt" }, - "unassignedItemsBanner": { - "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "unassignedItemsBannerNotice": { + "message": "Hinweis: Nicht zugeordnete Organisationseinträge sind nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." }, - "unassignedItemsBannerSelfHost": { - "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "unassignedItemsBannerSelfHostNotice": { + "message": "Hinweis: Ab dem 16. Mai 2024 sind nicht zugewiesene Organisationselemente nicht mehr in der Ansicht aller Tresore sichtbar und nur über die Administrator-Konsole zugänglich." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Weise diese Einträge einer Sammlung aus der", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "zu, um sie sichtbar zu machen.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Administrator-Konsole" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 36b14e447f..de0cfb3f6c 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Δωρεάν Διαχειριστής Κωδικών", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Ένας ασφαλής και δωρεάν διαχειριστής κωδικών, για όλες σας τις συσκευές.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Συνδεθείτε ή δημιουργήστε ένα νέο λογαριασμό για να αποκτήσετε πρόσβαση στο ασφαλές vault σας." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Αλλαγή Κύριου Κωδικού" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Προστέθηκε φάκελος" }, - "changeMasterPass": { - "message": "Αλλαγή Κύριου Κωδικού" - }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" - }, "twoStepLoginConfirmation": { "message": "Η σύνδεση σε δύο βήματα καθιστά πιο ασφαλή τον λογαριασμό σας, απαιτώντας να επαληθεύσετε τα στοιχεία σας με μια άλλη συσκευή, όπως κλειδί ασφαλείας, εφαρμογή επαλήθευσης, μήνυμα SMS, τηλεφωνική κλήση ή email. Μπορείτε να ενεργοποιήσετε τη σύνδεση σε δύο βήματα στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Κλειδώστε το vault" }, - "privateModeWarning": { - "message": "Η υποστήριξη ιδιωτικής λειτουργίας είναι πειραματική και ορισμένες δυνατότητες είναι περιορισμένες." - }, "customFields": { "message": "Προσαρμοσμένα Πεδία" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 36e3ce65a8..493a909f8a 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -374,12 +374,21 @@ "other": { "message": "Other" }, + "unlockMethods": { + "message": "Unlock options" + }, "unlockMethodNeededToChangeTimeoutActionDesc": { "message": "Set up an unlock method to change your vault timeout action." }, "unlockMethodNeeded": { "message": "Set up an unlock method in Settings" }, + "sessionTimeoutHeader": { + "message": "Session timeout" + }, + "otherOptions": { + "message": "Other options" + }, "rateExtension": { "message": "Rate the extension" }, @@ -1120,9 +1129,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,6 +3006,9 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, @@ -3023,6 +3032,9 @@ "adminConsole": { "message": "Admin Console" }, + "accountSecurity": { + "message": "Account security" + }, "errorAssigningTargetCollection": { "message": "Error assigning target collection." }, diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 1ac55feb42..a2ba76b3a6 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index cbe214f0b3..a95f11ffa1 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -220,7 +226,7 @@ "message": "Help & feedback" }, "helpCenter": { - "message": "Bitwarden Help center" + "message": "Bitwarden Help centre" }, "communityForums": { "message": "Explore Bitwarden community forums" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Added folder" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -728,7 +728,7 @@ "message": "Change the application's colour theme." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Change the application's colour theme. Applies to all logged in accounts." }, "dark": { "message": "Dark", @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -1168,7 +1165,7 @@ "message": "Show a recognizable image next to each login." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Show a recognisable image next to each login. Applies to all logged in accounts." }, "enableBadgeCounter": { "message": "Show badge counter" @@ -1733,7 +1730,7 @@ "message": "An organization policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "excludedDomains": { "message": "Excluded Domains" @@ -1993,7 +1990,7 @@ "message": "Your Master Password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "resetPasswordPolicyAutoEnroll": { "message": "Automatic Enrollment" @@ -2009,11 +2006,11 @@ "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -2040,7 +2037,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -2057,7 +2054,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2114,7 +2111,7 @@ "message": "Exporting Personal Vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2308,7 +2305,7 @@ } }, "autofillPageLoadPolicyActivated": { - "message": "Your organization policies have turned on auto-fill on page load." + "message": "Your organisation policies have turned on auto-fill on page load." }, "howToAutofill": { "message": "How to auto-fill" @@ -2380,7 +2377,7 @@ "message": "Approve with master password" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2691,7 +2688,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organisation items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organisation items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index ee5666f3cc..4ffbb147be 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden - Administrador de contraseñas", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden es un gestor de contraseñas seguro y gratuito para todos tus dispositivos.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifícate o crea una nueva cuenta para acceder a tu caja fuerte." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambiar contraseña maestra" }, + "continueToWebApp": { + "message": "¿Continuar a la aplicación web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puedes cambiar la contraseña maestra en la aplicación web de Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de huella digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Carpeta añadida" }, - "changeMasterPass": { - "message": "Cambiar contraseña maestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" - }, "twoStepLoginConfirmation": { "message": "La autenticación en dos pasos hace que tu cuenta sea mucho más segura, requiriendo que introduzcas un código de seguridad de una aplicación de autenticación cada vez que accedes. La autenticación en dos pasos puede ser habilitada en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear la caja fuerte" }, - "privateModeWarning": { - "message": "El soporte en modo privado es experimental y algunas características son limitadas." - }, "customFields": { "message": "Campos personalizados" }, @@ -2965,27 +2962,27 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "¿Quiere hacer de Bitwarden su gestor de contraseñas predeterminado?", + "message": "¿Hacer de Bitwarden su administrador de contraseñas predeterminado?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Pasar por alto esta opción puede causar conflictos entre el menú de relleno automático de Bitwarden y el del navegador.", + "message": "Pasar por alto esta opción puede causar conflictos entre el menú de autocompletar de Bitwarden y el de tu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Hacer de Bitwarden su gestor de contraseñas predeterminado", + "message": "Hacer de Bitwarden tu administrador de contraseñas predeterminado", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "No se pudo establecer Bitwarden como el gestor de contraseñas predeterminado", + "message": "No se puede establecer Bitwarden como el administrador de contraseñas predeterminado", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { - "message": "Debe otorgar los permisos de privacidad del navegador a Bitwarden para establecerlo como gestor de contraseñas predeterminado.", + "message": "Debes otorgar permisos de privacidad del navegador a Bitwarden para establecerlo como administrador de contraseñas predeterminado.", "description": "Description for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "makeDefault": { - "message": "Predeterminar", + "message": "Establecer como predeterminado", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { @@ -3000,16 +2997,36 @@ "message": "Se produjo un error al guardar las credenciales. Revise la consola para obtener detalles.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Éxito" + }, "removePasskey": { - "message": "Remove passkey" + "message": "Eliminar passkey" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Passkey eliminada" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Asignar estos elementos a una colección de", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para hcerlos visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Consola de administrador" + }, + "errorAssigningTargetCollection": { + "message": "Error al asignar la colección de destino." + }, + "errorAssigningTargetFolder": { + "message": "Error al asignar la carpeta de destino." } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index ea1758468e..0f5b164717 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Tasuta paroolihaldur", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvaline ja tasuta paroolihaldur kõikidele sinu seadmetele.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logi oma olemasolevasse kontosse sisse või loo uus konto." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Muuda ülemparooli" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kaust on lisatud" }, - "changeMasterPass": { - "message": "Muuda ülemparooli" - }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" - }, "twoStepLoginConfirmation": { "message": "Kaheastmeline kinnitamine aitab konto turvalisust tõsta. Lisaks paroolile pead kontole ligipääsemiseks kinnitama sisselogimise päringu SMS-ga, telefonikõnega, autentimise rakendusega või e-postiga. Kaheastmelist kinnitust saab sisse lülitada bitwarden.com veebihoidlas. Soovid seda kohe avada?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lukusta hoidla" }, - "privateModeWarning": { - "message": "Privaatrežiimi toetus on katsejärgus, mistõttu mõned funktsioonid on piiratud." - }, "customFields": { "message": "Kohandatud väljad" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Eemalda pääsuvõti" }, "passkeyRemoved": { "message": "Pääsuvõti on eemaldatud" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 529a1e8127..4bc1373201 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pasahitz kudeatzailea", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa da.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Saioa hasi edo sortu kontu berri bat zure kutxa gotorrera sartzeko." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Aldatu pasahitz nagusia" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Karpeta gehituta" }, - "changeMasterPass": { - "message": "Aldatu pasahitz nagusia" - }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" - }, "twoStepLoginConfirmation": { "message": "Bi urratseko saio hasiera dela eta, zure kontua seguruagoa da, beste aplikazio/gailu batekin saioa hastea eskatzen baitizu; adibidez, segurtasun-gako, autentifikazio-aplikazio, SMS, telefono dei edo email bidez. Bi urratseko saio hasiera bitwarden.com webgunean aktibatu daiteke. Orain joan nahi duzu webgunera?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blokeatu kutxa gotorra" }, - "privateModeWarning": { - "message": "Modu pribatuko euskarria esperimentala da eta ezaugarri batzuk mugatuak dira." - }, "customFields": { "message": "Eremu pertsonalizatuak" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 669eb151f4..8b09cb224f 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - مدیریت کلمه عبور رایگان", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "وارد شوید یا یک حساب کاربری بسازید تا به گاوصندوق امن‌تان دسترسی یابید." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "تغییر کلمه عبور اصلی" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "پوشه اضافه شد" }, - "changeMasterPass": { - "message": "تغییر کلمه عبور اصلی" - }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" - }, "twoStepLoginConfirmation": { "message": "ورود دو مرحله ای باعث می‌شود که حساب کاربری شما با استفاده از یک دستگاه دیگر مانند کلید امنیتی، برنامه احراز هویت، پیامک، تماس تلفنی و یا ایمیل، اعتبار خود را با ایمنی بیشتر اثبات کند. ورود دو مرحله ای می تواند در bitwarden.com فعال شود. آیا می‌خواهید از سایت بازدید کنید؟" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "قفل گاوصندوق" }, - "privateModeWarning": { - "message": "پشتیبانی حالت خصوصی آزمایشی است و برخی از ویژگی‌ها محدود هستند." - }, "customFields": { "message": "فیلدهای سفارشی" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 17aea532ba..95c77e0c09 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden – Ilmainen salasanahallinta", + "message": "Bitwarden Salasanahallinta", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi.", - "description": "Extension description" + "message": "Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi.", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Käytä salattua holviasi kirjautumalla sisään tai luo uusi tili." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Vaihda pääsalasana" }, + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." + }, "fingerprintPhrase": { "message": "Tunnistelauseke", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Kansio lisätty" }, - "changeMasterPass": { - "message": "Vaihda pääsalasana" - }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" - }, "twoStepLoginConfirmation": { "message": "Kaksivaiheinen kirjautuminen parantaa tilisi suojausta vaatimalla kirjautumisen vahvistuksen salasanan lisäksi todennuslaitteen, ‑sovelluksen, tekstiviestin, puhelun tai sähköpostin avulla. Voit ottaa kaksivaiheisen kirjautumisen käyttöön bitwarden.com‑verkkoholvissa. Haluatko avata sen nyt?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lukitse holvi" }, - "privateModeWarning": { - "message": "Yksityisen tilan tuki on kokeellinen ja jotkin ominaisuudet toimivat rajoitetusti." - }, "customFields": { "message": "Lisäkentät" }, @@ -2828,7 +2825,7 @@ "message": "Korvataanko suojausavain?" }, "overwritePasskeyAlert": { - "message": "Kohde sisältää jo suojausavaimen. Haluatko varmasti korvata nykyisen salasanan?" + "message": "Kohde sisältää jo suojausavaimen. Haluatko varmasti korvata nykyisen suojausavaimen?" }, "featureNotSupported": { "message": "Ominaisuutta ei vielä tueta" @@ -3000,16 +2997,36 @@ "message": "Virhe tallennettaessa käyttäjätietoja. Näet isätietoja hallinnasta.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Onnistui" + }, "removePasskey": { "message": "Poista suojausavain" }, "passkeyRemoved": { "message": "Suojausavain poistettiin" }, - "unassignedItemsBanner": { - "message": "Huomautus: Organisaatioiden kokoelmiin määrittämättömät kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "unassignedItemsBannerNotice": { + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." }, - "unassignedItemsBannerSelfHost": { - "message": "Huomautus: 2.5.2024 alkaen kokoelmiin määrittämättömät organisaatioiden kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "unassignedItemsBannerSelfHostNotice": { + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy \"Kaikki holvit\" -näkymässä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Määritä nämä kohteet kokoelmaan", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", jotta ne näkyvät.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Hallintapaneelista" + }, + "errorAssigningTargetCollection": { + "message": "Virhe määritettäessä kohdekokoelmaa." + }, + "errorAssigningTargetFolder": { + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index 42d5060e28..b536fa9c19 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Libreng Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Maglog-in o gumawa ng bagong account para ma-access ang iyong ligtas na kahadeyero." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Baguhin ang Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Idinagdag na folder" }, - "changeMasterPass": { - "message": "Palitan ang master password" - }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" - }, "twoStepLoginConfirmation": { "message": "Ang two-step login ay nagpapagaan sa iyong account sa pamamagitan ng pag-verify sa iyong login sa isa pang device tulad ng security key, authenticator app, SMS, tawag sa telepono o email. Ang two-step login ay maaaring magawa sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "I-lock ang vault" }, - "privateModeWarning": { - "message": "Ang suporta sa private mode ay eksperimental at limitado ang ilang mga tampok." - }, "customFields": { "message": "Pasadyang mga patlang" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 6cced1cb0d..541541ab54 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gestion des mots de passe", + "message": "Gestionnaire de mots de passe Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils.", - "description": "Extension description" + "message": "Chez vous, au travail, n'importe où, Bitwarden sécurise mots de passe, clés d'accès et informations sensibles", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Identifiez-vous ou créez un nouveau compte pour accéder à votre coffre sécurisé." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Changer le mot de passe principal" }, + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." + }, "fingerprintPhrase": { "message": "Phrase d'empreinte", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -525,13 +531,13 @@ "message": "Impossible de scanner le QR code à partir de la page web actuelle" }, "totpCaptureSuccess": { - "message": "Clé de l'Authentificateur ajoutée" + "message": "Clé Authenticator ajoutée" }, "totpCapture": { "message": "Scanner le QR code de l'authentificateur à partir de la page web actuelle" }, "copyTOTP": { - "message": "Copier la clé de l'Authentificateur (TOTP)" + "message": "Copier la clé Authenticator (TOTP)" }, "loggedOut": { "message": "Déconnecté" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dossier ajouté" }, - "changeMasterPass": { - "message": "Changer le mot de passe principal" - }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" - }, "twoStepLoginConfirmation": { "message": "L'authentification à deux facteurs rend votre compte plus sûr en vous demandant de vérifier votre connexion avec un autre dispositif tel qu'une clé de sécurité, une application d'authentification, un SMS, un appel téléphonique ou un courriel. L'authentification à deux facteurs peut être configurée dans le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" }, @@ -659,7 +659,7 @@ "message": "Liste les éléments des cartes de paiement sur la Page d'onglet pour faciliter la saisie automatique." }, "showIdentitiesCurrentTab": { - "message": "Afficher les identités sur la page d'onglet" + "message": "Afficher les identités sur la Page d'onglet" }, "showIdentitiesCurrentTabDesc": { "message": "Liste les éléments d'identité sur la Page d'onglet pour faciliter la saisie automatique." @@ -802,7 +802,7 @@ "message": "En savoir plus" }, "authenticatorKeyTotp": { - "message": "Clé de l'Authentificateur (TOTP)" + "message": "Clé Authenticator (TOTP)" }, "verificationCodeTotp": { "message": "Code de vérification (TOTP)" @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Verrouiller le coffre" }, - "privateModeWarning": { - "message": "La prise en charge de la navigation privée est expérimentale et certaines fonctionnalités sont limitées." - }, "customFields": { "message": "Champs personnalisés" }, @@ -3000,16 +2997,36 @@ "message": "Erreur lors de l'enregistrement des identifiants. Consultez la console pour plus de détails.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Succès" + }, "removePasskey": { "message": "Retirer la clé d'identification (passkey)" }, "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" }, - "unassignedItemsBanner": { - "message": "Notice : les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres et sont uniquement accessibles via la console d'administration. Assignez ces éléments à une collection à partir de la console d'administration pour les rendre visibles." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Remarque : au 2 mai 2024, les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Ajouter ces éléments à une collection depuis la", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "pour les rendre visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Console Admin" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 023e03b834..d23f0377bc 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -3,39 +3,39 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." }, "createAccount": { - "message": "Create account" + "message": "Crea unha conta" }, "login": { - "message": "Log in" + "message": "Iniciar sesión" }, "enterpriseSingleSignOn": { "message": "Enterprise single sign-on" }, "cancel": { - "message": "Cancel" + "message": "Cancelar" }, "close": { - "message": "Close" + "message": "Pechar" }, "submit": { "message": "Submit" }, "emailAddress": { - "message": "Email address" + "message": "Enderezo de correo electrónico" }, "masterPass": { - "message": "Master password" + "message": "Contrasinal mestre" }, "masterPassDesc": { "message": "The master password is the password you use to access your vault. It is very important that you do not forget your master password. There is no way to recover the password in the event that you forget it." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index 1e633f5eb9..0a9d8a8106 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - מנהל סיסמאות חינמי", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "צור חשבון חדש או התחבר כדי לגשת לכספת המאובטחת שלך." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "החלף סיסמה ראשית" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "נוספה תיקייה" }, - "changeMasterPass": { - "message": "החלף סיסמה ראשית" - }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" - }, "twoStepLoginConfirmation": { "message": "התחברות בשני-שלבים הופכת את החשבון שלך למאובטח יותר בכך שאתה נדרש לוודא בכל כניסה בעזרת מכשיר אחר כדוגמת מפתח אבטחה, תוכנת אימות, SMS, שיחת טלפון, או אימייל. ניתן להפעיל את \"התחברות בשני-שלבים\" בכספת שבאתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "נעל את הכספת" }, - "privateModeWarning": { - "message": "המצב הפרטי הוא במסגרת ניסוי וחלק מהיכולות מוגבלות." - }, "customFields": { "message": "שדות מותאמים אישית" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 44f645bc47..042ef96ea2 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3,12 +3,12 @@ "message": "bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "अपनी सुरक्षित तिजोरी में प्रवेश करने के लिए नया खाता बनाएं या लॉग इन करें।" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "जोड़ा गया फ़ोल्डर" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "आप वेब वॉल्ट bitwarden.com पर अपना मास्टर पासवर्ड बदल सकते हैं।क्या आप अब वेबसाइट पर जाना चाहते हैं?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "वॉल्ट लॉक करें" }, - "privateModeWarning": { - "message": "निजी मोड समर्थन प्रायोगिक है और कुछ सुविधाएँ सीमित हैं।" - }, "customFields": { "message": "Custom Fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index e74a72bc4f..09388672ff 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - besplatni upravitelj lozinki", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden je siguran i besplatan upravitelj lozinki za sve tvoje uređaje.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavi se ili stvori novi račun za pristup svojem sigurnom trezoru." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Promjeni glavnu lozinku" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Jedinstvena fraza", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Promjeni glavnu lozinku" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" - }, "twoStepLoginConfirmation": { "message": "Prijava dvostrukom autentifikacijom čini tvoj račun još sigurnijim tako što će zahtijevati da potvrdiš prijavu putem drugog uređaja pomoću sigurnosnog koda, autentifikatorske aplikacije, SMS-om, pozivom ili e-poštom. Prijavu dvostrukom autentifikacijom možeš omogućiti na web trezoru. Želiš li sada posjetiti bitwarden.com?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zaključaj trezor" }, - "privateModeWarning": { - "message": "Podrška za privatni način rada je eksperimentalna, a neke su značajke ograničene." - }, "customFields": { "message": "Prilagođena polja" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index cadd72a475..5647f5d97d 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ingyenes jelszókezelő", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Egy biztonságos és ingyenes jelszókezelő az összes eszközre.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Bejelentkezés vagy új fiók létrehozása a biztonsági széf eléréséhez." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mesterjelszó módosítása" }, + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." + }, "fingerprintPhrase": { "message": "Ujjlenyomat kifejezés", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "A mappa hozzáadásra került." }, - "changeMasterPass": { - "message": "Mesterjelszó módosítása" - }, - "changeMasterPasswordConfirmation": { - "message": "Mesterjelszavadat a bitwarden.com webes széfén tudod megváltoztatni. Szeretnéd meglátogatni a most a weboldalt?" - }, "twoStepLoginConfirmation": { "message": "A kétlépcsős bejelentkezés biztonságosabbá teszi a fiókot azáltal, hogy ellenőrizni kell a bejelentkezést egy másik olyan eszközzel mint például biztonsági kulcs, hitelesítő alkalmazás, SMS, telefon hívás vagy email. A kétlépcsős bejelentkezést a bitwarden.com webes széfben lehet engedélyezni. Felkeressük a webhelyet most?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "A széf zárolása" }, - "privateModeWarning": { - "message": "A privát mód támogatása kísérleti és néhány funkció korlátozott." - }, "customFields": { "message": "Egyedi mezők" }, @@ -3000,16 +2997,36 @@ "message": "Hiba történt a hitelesítések mentésekor. A részletekért ellenőrizzük a konzolt.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sikeres" + }, "removePasskey": { "message": "Jelszó eltávolítása" }, "passkeyRemoved": { "message": "A jelszó eltávolításra került." }, - "unassignedItemsBanner": { - "message": "Megjegyzés: A nem hozzá nem rendelt szervezeti elemek már nem láthatók az Összes széf nézetben és csak az Adminisztrátori konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátor konzolból, hogy láthatóvá tegyük azokat." + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." }, - "unassignedItemsBannerSelfHost": { - "message": "Figyelmeztetés: 2024. május 2-án a nem hozzá rendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak a Felügyeleti konzolon keresztül lesznek elérhetők. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyük azokat." + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "a láthatósághoz.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Adminisztrátori konzol" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 9907a7520c..37961ba9a3 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Pengelola Sandi Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden adalah sebuah pengelola sandi yang aman dan gratis untuk semua perangkat Anda.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Masuk atau buat akun baru untuk mengakses brankas Anda." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ubah Kata Sandi Utama" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Frasa Sidik Jari", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Tambah Folder" }, - "changeMasterPass": { - "message": "Ubah Kata Sandi Utama" - }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" - }, "twoStepLoginConfirmation": { "message": "Info masuk dua langkah membuat akun Anda lebih aman dengan mengharuskan Anda memverifikasi info masuk Anda dengan peranti lain seperti kode keamanan, aplikasi autentikasi, SMK, panggilan telepon, atau email. Info masuk dua langkah dapat diaktifkan di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kunci brankas" }, - "privateModeWarning": { - "message": "Dukungan mode pribadi bersifat eksperimental dan beberapa fitur terbatas." - }, "customFields": { "message": "Ruas Khusus" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index 65a5a1ad04..9420bca6ef 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Password Manager Gratis", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un password manager sicuro e gratis per tutti i tuoi dispositivi.", - "description": "Extension description" + "message": "A casa, al lavoro, o in viaggio, Bitwarden protegge tutte le tue password, passkey, e informazioni sensibili", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Accedi o crea un nuovo account per accedere alla tua cassaforte." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Cambia password principale" }, + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." + }, "fingerprintPhrase": { "message": "Frase impronta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Cartella aggiunta" }, - "changeMasterPass": { - "message": "Cambia password principale" - }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" - }, "twoStepLoginConfirmation": { "message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blocca la cassaforte" }, - "privateModeWarning": { - "message": "Il supporto della modalità privata è sperimentale e alcune funzionalità sono limitate." - }, "customFields": { "message": "Campi personalizzati" }, @@ -3000,16 +2997,36 @@ "message": "Errore durante il salvataggio delle credenziali. Controlla la console per più dettagli.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Successo" + }, "removePasskey": { "message": "Rimuovi passkey" }, "passkeyRemoved": { "message": "Passkey rimossa" }, - "unassignedItemsBanner": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "unassignedItemsBannerNotice": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." }, - "unassignedItemsBannerSelfHost": { - "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 16 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per renderli visibili.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Console di amministrazione" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index 05fb7fe5de..d1429a40e0 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 無料パスワードマネージャー", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden はあらゆる端末で使える、安全な無料パスワードマネージャーです。", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "安全なデータ保管庫へアクセスするためにログインまたはアカウントを作成してください。" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "マスターパスワードの変更" }, + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" + }, "fingerprintPhrase": { "message": "パスフレーズ", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "フォルダを追加しました" }, - "changeMasterPass": { - "message": "マスターパスワードの変更" - }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" - }, "twoStepLoginConfirmation": { "message": "2段階認証を使うと、ログイン時にセキュリティキーや認証アプリ、SMS、電話やメールでの認証を必要にすることでアカウントをさらに安全に出来ます。2段階認証は bitwarden.com ウェブ保管庫で有効化できます。ウェブサイトを開きますか?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "保管庫をロック" }, - "privateModeWarning": { - "message": "プライベートモードのサポートは実験的であり、一部機能は制限されています。" - }, "customFields": { "message": "カスタムフィールド" }, @@ -3000,16 +2997,36 @@ "message": "資格情報の保存中にエラーが発生しました。詳細はコンソールを確認してください。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "成功" + }, "removePasskey": { "message": "パスキーを削除" }, "passkeyRemoved": { "message": "パスキーを削除しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、すべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、すべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになります。" }, - "unassignedItemsBannerSelfHost": { - "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、すべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "で実行すると表示できるようになります。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "管理コンソール" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index d67b88ba9c..416018cc92 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index 61cfadc762..f5e3b8d334 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -3,12 +3,12 @@ "message": "ಬಿಟ್ವಾರ್ಡೆನ್" }, "extName": { - "message": "ಬಿಟ್‌ವಾರ್ಡೆನ್ - ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ನಿಮ್ಮ ಸುರಕ್ಷಿತ ವಾಲ್ಟ್ ಅನ್ನು ಪ್ರವೇಶಿಸಲು ಲಾಗ್ ಇನ್ ಮಾಡಿ ಅಥವಾ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಿ." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ಫೋಲ್ಡರ್ ಸೇರಿಸಿ" }, - "changeMasterPass": { - "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" - }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" - }, "twoStepLoginConfirmation": { "message": "ಭದ್ರತಾ ಕೀ, ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಇಮೇಲ್‌ನಂತಹ ಮತ್ತೊಂದು ಸಾಧನದೊಂದಿಗೆ ನಿಮ್ಮ ಲಾಗಿನ್ ಅನ್ನು ಪರಿಶೀಲಿಸುವ ಅಗತ್ಯವಿರುವ ಎರಡು ಹಂತದ ಲಾಗಿನ್ ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಹೆಚ್ಚು ಸುರಕ್ಷಿತಗೊಳಿಸುತ್ತದೆ. ಬಿಟ್ವಾರ್ಡೆನ್.ಕಾಮ್ ವೆಬ್ ವಾಲ್ಟ್ನಲ್ಲಿ ಎರಡು-ಹಂತದ ಲಾಗಿನ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ವಾಲ್ಟ್ ಅನ್ನು ಲಾಕ್ ಮಾಡಿ" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "ಕಸ್ಟಮ್ ಕ್ಷೇತ್ರಗಳು" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index c71fbdf7a8..8b34a76833 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 무료 비밀번호 관리자", + "message": "Bitwarden 비밀번호 관리자", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자입니다.", - "description": "Extension description" + "message": "집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다.", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "안전 보관함에 접근하려면 로그인하거나 새 계정을 만드세요." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "마스터 비밀번호 변경" }, + "continueToWebApp": { + "message": "웹 앱에서 계속하시겠용?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden 웹 앱에서 마스터 비밀번호를 변경할 수 있습니다." + }, "fingerprintPhrase": { "message": "지문 구절", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -223,10 +229,10 @@ "message": "Bitwarden 도움말 센터" }, "communityForums": { - "message": "Explore Bitwarden community forums" + "message": "Bitwarden 커뮤니티 포럼 탐색하기" }, "contactSupport": { - "message": "Contact Bitwarden support" + "message": "Bitwarden 지원에 문의하기" }, "sync": { "message": "동기화" @@ -269,7 +275,7 @@ "message": "길이" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "최소 비밀번호 길이" }, "uppercase": { "message": "대문자 (A-Z)" @@ -327,7 +333,7 @@ "message": "비밀번호" }, "totp": { - "message": "Authenticator secret" + "message": "인증기 비밀 키" }, "passphrase": { "message": "패스프레이즈" @@ -369,10 +375,10 @@ "message": "기타" }, "unlockMethodNeededToChangeTimeoutActionDesc": { - "message": "Set up an unlock method to change your vault timeout action." + "message": "잠금 해제 방법을 설정하여 보관함의 시간 초과 동작을 변경하세요." }, "unlockMethodNeeded": { - "message": "Set up an unlock method in Settings" + "message": "설정에서 잠금 해제 수단 설정하기" }, "rateExtension": { "message": "확장 프로그램 평가" @@ -415,7 +421,7 @@ "message": "지금 잠그기" }, "lockAll": { - "message": "Lock all" + "message": "모두 잠그기" }, "immediately": { "message": "즉시" @@ -478,7 +484,7 @@ "message": "마스터 비밀번호를 재입력해야 합니다." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "마스터 비밀번호는 최소 $VALUE$자 이상이어야 합니다.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -494,10 +500,10 @@ "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "로그인에 성공했습니다." }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "이제 창을 닫으실 수 있습니다." }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." @@ -522,16 +528,16 @@ "message": "선택한 항목을 이 페이지에서 자동 완성할 수 없습니다. 대신 정보를 직접 복사 / 붙여넣기하여 사용하십시오." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "현재 웹페이지에서 QR 코드를 스캔할 수 없습니다" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "인증 키를 추가했습니다" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "현재 웹페이지에서 QR 코드 스캔하기" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "인증서 키 (TOTP) 복사" }, "loggedOut": { "message": "로그아웃됨" @@ -557,12 +563,6 @@ "addedFolder": { "message": "폴더 추가함" }, - "changeMasterPass": { - "message": "마스터 비밀번호 변경" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" - }, "twoStepLoginConfirmation": { "message": "2단계 인증은 보안 키, 인증 앱, SMS, 전화 통화 등의 다른 기기로 사용자의 로그인 시도를 검증하여 사용자의 계정을 더욱 안전하게 만듭니다. 2단계 인증은 bitwarden.com 웹 보관함에서 활성화할 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" }, @@ -644,7 +644,7 @@ "description": "This is the folder for uncategorized items" }, "enableAddLoginNotification": { - "message": "Ask to add login" + "message": "로그인을 추가할 건지 물어보기" }, "addLoginNotificationDesc": { "message": "\"로그인 추가 알림\"을 사용하면 새 로그인을 사용할 때마다 보관함에 그 로그인을 추가할 것인지 물어봅니다." @@ -653,7 +653,7 @@ "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." }, "showCardsCurrentTab": { - "message": "Show cards on Tab page" + "message": "탭 페이지에 카드 표시" }, "showCardsCurrentTabDesc": { "message": "List card items on the Tab page for easy auto-fill." @@ -679,7 +679,7 @@ "message": "예, 지금 저장하겠습니다." }, "enableChangedPasswordNotification": { - "message": "Ask to update existing login" + "message": "현재 로그인으로 업데이트할 건지 묻기" }, "changedPasswordNotificationDesc": { "message": "Ask to update a login's password when a change is detected on a website." @@ -688,10 +688,10 @@ "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "패스키를 저장 및 사용할지 묻기" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "보관함에 새 패스키를 저장하거나 로그인할지 물어봅니다. 모든 로그인된 계정에 적용됩니다." }, "notificationChangeDesc": { "message": "Bitwarden에 저장되어 있는 비밀번호를 이 비밀번호로 변경하시겠습니까?" @@ -703,7 +703,7 @@ "message": "Unlock your Bitwarden vault to complete the auto-fill request." }, "notificationUnlock": { - "message": "Unlock" + "message": "잠금 해제" }, "enableContextMenuItem": { "message": "Show context menu options" @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "보관함 잠그기" }, - "privateModeWarning": { - "message": "시크릿 모드 지원은 실험적이며 일부 기능이 제한됩니다." - }, "customFields": { "message": "사용자 지정 필드" }, @@ -2786,55 +2783,55 @@ "message": "Confirm file password" }, "typePasskey": { - "message": "Passkey" + "message": "패스키" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "패스키가 복사되지 않습니다" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "패스키는 복제된 아이템에 복사되지 않습니다. 계속 이 항목을 복제하시겠어요?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Verification required by the initiating site. This feature is not yet implemented for accounts without master password." + "message": "사이트에서 인증을 요구합니다. 이 기능은 비밀번호가 없는 계정에서는 아직 지원하지 않습니다." }, "logInWithPasskey": { - "message": "Log in with passkey?" + "message": "패스키로 로그인하시겠어요?" }, "passkeyAlreadyExists": { - "message": "A passkey already exists for this application." + "message": "이미 이 애플리케이션에 해당하는 패스키가 있습니다." }, "noPasskeysFoundForThisApplication": { - "message": "No passkeys found for this application." + "message": "이 애플리케이션에 대한 패스키를 찾을 수 없습니다." }, "noMatchingPasskeyLogin": { - "message": "You do not have a matching login for this site." + "message": "사이트와 일치하는 로그인이 없습니다." }, "confirm": { "message": "Confirm" }, "savePasskey": { - "message": "Save passkey" + "message": "패스키 저장" }, "savePasskeyNewLogin": { - "message": "Save passkey as new login" + "message": "새 로그인으로 패스키 저장" }, "choosePasskey": { - "message": "Choose a login to save this passkey to" + "message": "패스키를 저장할 로그인 선택하기" }, "passkeyItem": { - "message": "Passkey Item" + "message": "패스키 항목" }, "overwritePasskey": { - "message": "Overwrite passkey?" + "message": "비밀번호를 덮어쓰시겠어요?" }, "overwritePasskeyAlert": { - "message": "This item already contains a passkey. Are you sure you want to overwrite the current passkey?" + "message": "이 항목은 이미 패스키가 있습니다. 정말로 현재 패스키를 덮어쓰시겠어요?" }, "featureNotSupported": { "message": "Feature not yet supported" }, "yourPasskeyIsLocked": { - "message": "Authentication required to use passkey. Verify your identity to continue." + "message": "패스키를 사용하려면 인증이 필요합니다. 인증을 진행해주세요." }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { - "message": "Remove passkey" + "message": "패스키 제거" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "패스키 제거됨" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index 0fc146c250..6ffbf522d3 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prisijunkite arba sukurkite naują paskyrą, kad galėtumėte pasiekti saugyklą." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Keisti pagrindinį slaptažodį" }, + "continueToWebApp": { + "message": "Tęsti į žiniatinklio programėlę?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pagrindinį slaptažodį galite pakeisti „Bitwarden“ žiniatinklio programėlėje." + }, "fingerprintPhrase": { "message": "Pirštų atspaudų frazė", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Katalogas pridėtas" }, - "changeMasterPass": { - "message": "Keisti pagrindinį slaptažodį" - }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti „bitwarden.com“ žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" - }, "twoStepLoginConfirmation": { "message": "Prisijungus dviem veiksmais, jūsų paskyra tampa saugesnė, reikalaujant patvirtinti prisijungimą naudojant kitą įrenginį, pvz., saugos raktą, autentifikavimo programėlę, SMS, telefono skambutį ar el. paštą. Dviejų žingsnių prisijungimą galima įjungti „bitwarden.com“ interneto saugykloje. Ar norite dabar apsilankyti svetainėje?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Užrakinti saugyklą" }, - "privateModeWarning": { - "message": "Privataus režimo palaikymas yra eksperimentinis, o kai kurios funkcijos yra ribotos." - }, "customFields": { "message": "Pasirinktiniai laukai" }, @@ -3000,16 +2997,36 @@ "message": "Klaida išsaugant kredencialus. Išsamesnės informacijos patikrink konsolėje.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sėkmė" + }, "removePasskey": { "message": "Pašalinti slaptaraktį" }, "passkeyRemoved": { "message": "Pašalintas slaptaraktis" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Pranešimas: nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Pranešimas: 2024 m. gegužės 16 d. nepriskirti organizacijos elementai nebėra matomi peržiūros rodinyje Visi saugyklos ir yra pasiekiami tik per Administratoriaus konsolę." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priskirkite šiuos elementus kolekcijai iš", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", kad jie būtų matomi.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Administratoriaus konsolės" + }, + "errorAssigningTargetCollection": { + "message": "Klaida priskiriant tikslinę kolekciją." + }, + "errorAssigningTargetFolder": { + "message": "Klaida priskiriant tikslinį aplanką." } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index efac417556..d8e42a1c50 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Drošs bezmaksas paroļu pārvaldnieks visām Tavām ierīcēm.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Jāpiesakās vai jāizveido jauns konts, lai piekļūtu drošajai glabātavai." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Mainīt galveno paroli" }, + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." + }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pievienoja mapi" }, - "changeMasterPass": { - "message": "Mainīt galveno paroli" - }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" - }, "twoStepLoginConfirmation": { "message": "Divpakāpju pieteikšanās padara kontu krietni drošāku, pieprasot apstiprināt pieteikšanos ar tādu citu ierīču vai pakalpojumu starpniecību kā drošības atslēga, autentificētāja lietotne, īsziņa, tālruņa zvans vai e-pasts. Divpakāpju pieteikšanos var iespējot bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Aizslēgt glabātavu" }, - "privateModeWarning": { - "message": "Personiskā stāvokļa atbalsts ir izmēģinājuma, un dažas iespējas ir ierobežotas." - }, "customFields": { "message": "Pielāgoti lauki" }, @@ -3000,16 +2997,36 @@ "message": "Kļūda piekļuves informācijas saglabāšanā. Jāpārbauda, vai konsolē ir izvērstāka informācija.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Izdevās" + }, "removePasskey": { "message": "Noņemt piekļuves atslēgu" }, "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" }, - "unassignedItemsBanner": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" un ir pieejami tikai pārvaldības konsolē." }, - "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "lai padarītu tos redzamus.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "pārvaldības konsolē," + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 9b66e6f0d6..aa4fea96e3 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ ", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "നിങ്ങളുടെ സുരക്ഷിത വാൾട്ടിലേക്കു പ്രവേശിക്കാൻ ലോഗിൻ ചെയ്യുക അല്ലെങ്കിൽ ഒരു പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "ചേർക്കപ്പെട്ട ഫോൾഡർ" }, - "changeMasterPass": { - "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" - }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു ബിറ്റ് വാർഡൻ വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" - }, "twoStepLoginConfirmation": { "message": "സുരക്ഷാ കീ, ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ, SMS, ഫോൺ കോൾ അല്ലെങ്കിൽ ഇമെയിൽ പോലുള്ള മറ്റൊരു ഉപകരണം ഉപയോഗിച്ച് തങ്ങളുടെ ലോഗിൻ സ്ഥിരീകരിക്കാൻ ആവശ്യപ്പെടുന്നതിലൂടെ രണ്ട്-ഘട്ട ലോഗിൻ തങ്ങളുടെ അക്കൗണ്ടിനെ കൂടുതൽ സുരക്ഷിതമാക്കുന്നു. bitwarden.com വെബ് വാൾട്ടിൽ രണ്ട്-ഘട്ട ലോഗിൻ പ്രവർത്തനക്ഷമമാക്കാനാകും.തങ്ങള്ക്കു ഇപ്പോൾ വെബ്സൈറ്റ് സന്ദർശിക്കാൻ ആഗ്രഹമുണ്ടോ?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "നിലവറ പൂട്ടുക" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "ഇഷ്‌ടാനുസൃത ഫീൽഡുകൾ" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index f9f37b2511..463309789d 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - विनामूल्य पासवर्ड व्यवस्थापक", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "तुमच्या सर्व उपकरणांसाठी एक सुरक्षित व विनामूल्य पासवर्ड व्यवस्थापक.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "तुमच्या सुरक्षित तिजोरीत पोहचण्यासाठी लॉग इन करा किंवा नवीन खाते उघडा." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "मुख्य पासवर्ड बदला" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "अंगुलिमुद्रा वाक्यांश", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index 82d847ff0f..ea7b939f63 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden — Fri passordbehandling", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden er en sikker og fri passordbehandler for alle dine PCer og mobiler.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logg på eller opprett en ny konto for å få tilgang til ditt sikre hvelv." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Endre hovedpassordet" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "La til en mappe" }, - "changeMasterPass": { - "message": "Endre hovedpassordet" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" - }, "twoStepLoginConfirmation": { "message": "2-trinnsinnlogging gjør kontoen din mer sikker, ved å kreve at du verifiserer din innlogging med en annen enhet, f.eks. en autentiseringsapp, SMS, e-post, telefonsamtale, eller sikkerhetsnøkkel. 2-trinnsinnlogging kan aktiveres i netthvelvet ditt på bitwarden.com. Vil du besøke bitwarden.com nå?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås hvelvet" }, - "privateModeWarning": { - "message": "Støtte for privatmodus er eksperimentelt, og noen funksjoner er begrenset." - }, "customFields": { "message": "Tilpassede felter" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index 13d59c4546..aa01ef2c67 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis wachtwoordbeheer", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in of maak een nieuw account aan om toegang te krijgen tot je beveiligde kluis." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Hoofdwachtwoord wijzigen" }, + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." + }, "fingerprintPhrase": { "message": "Vingerafdrukzin", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -525,7 +531,7 @@ "message": "Kan de QR-code van de huidige webpagina niet scannen" }, "totpCaptureSuccess": { - "message": "Authenticatie-sleutel toegevoegd" + "message": "Authenticatiesleutel toegevoegd" }, "totpCapture": { "message": "Scan de authenticatie-QR-code van de huidige webpagina" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Map is toegevoegd" }, - "changeMasterPass": { - "message": "Hoofdwachtwoord wijzigen" - }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" - }, "twoStepLoginConfirmation": { "message": "Tweestapsaanmelding beschermt je account door je inlogpoging te bevestigen met een ander apparaat zoals een beveiligingscode, authenticatie-app, SMS, spraakoproep of e-mail. Je kunt Tweestapsaanmelding inschakelen in de webkluis op bitwarden.com. Wil je de website nu bezoeken?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kluis vergrendelen" }, - "privateModeWarning": { - "message": "Private mode ondersteuning is experimenteel en sommige functies zijn beperkt." - }, "customFields": { "message": "Aangepaste velden" }, @@ -1673,10 +1670,10 @@ "message": "Browserintegratie is niet ingeschakeld in de Bitwarden-desktopapplicatie. Schakel deze optie in de instellingen binnen de desktop-applicatie in." }, "startDesktopTitle": { - "message": "Bitwarden-desktopapplicatie opstarten" + "message": "Bitwarden desktopapplicatie opstarten" }, "startDesktopDesc": { - "message": "Je moet de Bitwarden-desktopapplicatie starten om deze functie te gebruiken." + "message": "Je moet de Bitwarden desktopapplicatie starten om deze functie te gebruiken." }, "errorEnableBiometricTitle": { "message": "Kon biometrie niet inschakelen" @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Succes" + }, "removePasskey": { "message": "Passkey verwijderen" }, "passkeyRemoved": { "message": "Passkey verwijderd" }, - "unassignedItemsBanner": { - "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluisjes en zijn alleen toegankelijk via de Admin Console. Om deze items zichtbaar te maken, moet je ze toewijzen aan een collectie via de Admin Console." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index e4b97ec956..83e19315e8 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - darmowy menedżer haseł", + "message": "Menedżer Haseł Bitwarden", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń.", - "description": "Extension description" + "message": "W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza Twoje hasła, passkeys i poufne informacje", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Zaloguj się lub utwórz nowe konto, aby uzyskać dostęp do Twojego bezpiecznego sejfu." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmień hasło główne" }, + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." + }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder został dodany" }, - "changeMasterPass": { - "message": "Zmień hasło główne" - }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" - }, "twoStepLoginConfirmation": { "message": "Logowanie dwustopniowe sprawia, że konto jest bardziej bezpieczne poprzez wymuszenie potwierdzenia logowania z innego urządzenia, takiego jak z klucza bezpieczeństwa, aplikacji uwierzytelniającej, wiadomości SMS, telefonu lub adresu e-mail. Logowanie dwustopniowe możesz włączyć w sejfie internetowym bitwarden.com. Czy chcesz przejść do tej strony?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zablokuj sejf" }, - "privateModeWarning": { - "message": "Obsługa trybu prywatnego jest eksperymentalna, a niektóre funkcje są ograniczone." - }, "customFields": { "message": "Pola niestandardowe" }, @@ -2709,7 +2706,7 @@ "message": "Otwórz rozszerzenie w nowym oknie, aby dokończyć logowanie." }, "popoutExtension": { - "message": "Popout extension" + "message": "Otwórz rozszerzenie w nowym oknie" }, "launchDuo": { "message": "Uruchom DUO" @@ -2822,7 +2819,7 @@ "message": "Wybierz dane logowania do których przypisać passkey" }, "passkeyItem": { - "message": "Passkey Item" + "message": "Element Passkey" }, "overwritePasskey": { "message": "Zastąpić passkey?" @@ -3000,16 +2997,36 @@ "message": "Błąd podczas zapisywania danych logowania. Sprawdź konsolę, aby uzyskać szczegóły.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sukces" + }, "removePasskey": { "message": "Usuń passkey" }, "passkeyRemoved": { "message": "Passkey został usunięty" }, - "unassignedItemsBanner": { - "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy i są dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy i są teraz dostępne tylko przez Konsolę Administracyjną." }, - "unassignedItemsBannerSelfHost": { - "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby były widoczne.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Konsola Administracyjna" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index a4e0688e3d..207c890130 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Gerenciador de Senhas", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gerenciador de senhas seguro e gratuito para todos os seus dispositivos.", - "description": "Extension description" + "message": "Em qual lugar for, o Bitwarden protege suas senhas, chaves de acesso, e informações confidenciais", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie a sessão ou crie uma nova conta para acessar seu cofre seguro." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar Senha Mestra" }, + "continueToWebApp": { + "message": "Continuar no aplicativo web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." + }, "fingerprintPhrase": { "message": "Frase Biométrica", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -494,10 +500,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar Senha Mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "O login de duas etapas torna a sua conta mais segura ao exigir que digite um código de segurança de um aplicativo de autenticação quando for iniciar a sessão. O login de duas etapas pode ser ativado no cofre web bitwarden.com. Deseja visitar o site agora?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear o cofre" }, - "privateModeWarning": { - "message": "O suporte para modo privado é experimental e alguns recursos são limitados." - }, "customFields": { "message": "Campos Personalizados" }, @@ -1500,7 +1497,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithBiometrics": { "message": "Desbloquear com a biometria" @@ -2005,7 +2002,7 @@ "message": "Selecionar pasta..." }, "noFoldersFound": { - "message": "No folders found", + "message": "Nenhuma pasta encontrada", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2017,7 +2014,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "hours": { @@ -2652,40 +2649,40 @@ } }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "total": { "message": "Total" @@ -2700,19 +2697,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "popoutTheExtensionToCompleteLogin": { - "message": "Popout the extension to complete login." + "message": "Abra a extensão para concluir o login." }, "popoutExtension": { - "message": "Popout extension" + "message": "Extensão pop-out" }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2846,13 +2843,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2965,51 +2962,71 @@ "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Make Bitwarden your default password manager?", + "message": "Tornar o Bitwarden seu gerenciador de senhas padrão?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Ignoring this option may cause conflicts between the Bitwarden auto-fill menu and your browser's.", + "message": "Ignorar esta opção pode causar conflitos entre o menu de autopreenchimento do Bitwarden e o do seu navegador.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Make Bitwarden your default password manager", + "message": "Faça do Bitwarden seu gerenciador de senhas padrão", "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": "Não é possível definir o Bitwarden como o gerenciador de senhas padrão", "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": "Você deve conceder permissões de privacidade do navegador ao Bitwarden para defini-lo como o Gerenciador de Senhas padrão.", "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": "Tornar padrão", "description": "Button text for the setting that allows overriding the default browser autofill settings" }, "saveCipherAttemptSuccess": { - "message": "Credentials saved successfully!", + "message": "Credenciais salvas com sucesso!", "description": "Notification message for when saving credentials has succeeded." }, "updateCipherAttemptSuccess": { - "message": "Credentials updated successfully!", + "message": "Credenciais atualizadas com sucesso!", "description": "Notification message for when updating credentials has succeeded." }, "saveCipherAttemptFailed": { - "message": "Error saving credentials. Check console for details.", + "message": "Erro ao salvar credenciais. Verifique o console para detalhes.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Sucesso" + }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Aviso: Itens da organização não atribuídos não estão mais visíveis na visualização Todos os Cofres e só são acessíveis por meio do painel de administração." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: Em 16 de maio, 2024, itens da organização não serão mais visíveis na visualização Todos os Cofres e só serão acessíveis por meio do painel de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para torná-los visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Painel de administração" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index c35531e445..26828b348a 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -7,8 +7,8 @@ "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos.", - "description": "Extension description" + "message": "Em casa, no trabalho, em todo o lado, o Bitwarden protege todas as suas palavras-passe e informações sensíveis", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Inicie sessão ou crie uma nova conta para aceder ao seu cofre seguro." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Alterar palavra-passe mestra" }, + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." + }, "fingerprintPhrase": { "message": "Frase de impressão digital", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -297,10 +303,10 @@ "message": "Incluir número" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos" + "message": "Mínimo de caracteres especiais" }, "avoidAmbChar": { "message": "Evitar caracteres ambíguos" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pasta adicionada" }, - "changeMasterPass": { - "message": "Alterar palavra-passe mestra" - }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" - }, "twoStepLoginConfirmation": { "message": "A verificação de dois passos torna a sua conta mais segura, exigindo que verifique o seu início de sessão com outro dispositivo, como uma chave de segurança, aplicação de autenticação, SMS, chamada telefónica ou e-mail. A verificação de dois passos pode ser configurada em bitwarden.com. Pretende visitar o site agora?" }, @@ -1064,7 +1064,7 @@ "message": "Editar as definições do navegador." }, "autofillOverlayVisibilityOff": { - "message": "Desligado", + "message": "Desativado", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Bloquear o cofre" }, - "privateModeWarning": { - "message": "O suporte do modo privado é experimental e algumas funcionalidades são limitadas." - }, "customFields": { "message": "Campos personalizados" }, @@ -1279,7 +1276,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -1303,7 +1300,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -1443,7 +1440,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -3000,16 +2997,36 @@ "message": "Erro ao guardar as credenciais. Verifique a consola para obter detalhes.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Com sucesso" + }, "removePasskey": { "message": "Remover chave de acesso" }, "passkeyRemoved": { "message": "Chave de acesso removida" }, - "unassignedItemsBanner": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres e só são acessíveis através da Consola de administração." }, - "unassignedItemsBannerSelfHost": { - "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de ser visíveis na vista Todos os cofres e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres e só estarão acessíveis através da consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para os tornar visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Consola de administração" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 885d70ca93..8bcf1a8430 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Manager de parole gratuit", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Un manager de parole sigur și gratuit pentru toate dispozitivele dvs.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Autentificați-vă sau creați un cont nou pentru a accesa seiful dvs. securizat." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Schimbare parolă principală" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fraza amprentă", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Dosar adăugat" }, - "changeMasterPass": { - "message": "Schimbare parolă principală" - }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală în seiful web bitwarden.com. Doriți să vizitați saitul acum?" - }, "twoStepLoginConfirmation": { "message": "Autentificarea în două etape vă face contul mai sigur, prin solicitarea unei verificări de autentificare cu un alt dispozitiv, cum ar fi o cheie de securitate, o aplicație de autentificare, un SMS, un apel telefonic sau un e-mail. Autentificarea în două etape poate fi configurată în seiful web bitwarden.com. Doriți să vizitați site-ul web acum?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Blocare seif" }, - "privateModeWarning": { - "message": "Suportul pentru modul privat este experimental, iar unele caracteristici sunt limitate." - }, "customFields": { "message": "Câmpuri particularizate" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index 69d9ca200f..23d69f44ba 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатный менеджер паролей", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Защищенный и бесплатный менеджер паролей для всех ваших устройств.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Войдите или создайте новый аккаунт для доступа к вашему защищенному хранилищу." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Изменить мастер-пароль" }, + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза отпечатка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Папка добавлена" }, - "changeMasterPass": { - "message": "Изменить мастер-пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" - }, "twoStepLoginConfirmation": { "message": "Двухэтапная аутентификация делает аккаунт более защищенным, поскольку требуется подтверждение входа при помощи другого устройства, например, ключа безопасности, приложения-аутентификатора, SMS, телефонного звонка или электронной почты. Двухэтапная аутентификация включается на bitwarden.com. Перейти на сайт сейчас?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблокировать хранилище" }, - "privateModeWarning": { - "message": "Частный режим - экспериментальный, некоторые функции ограничены." - }, "customFields": { "message": "Пользовательские поля" }, @@ -3000,16 +2997,36 @@ "message": "Ошибка сохранения учетных данных. Проверьте консоль для получения подробной информации.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успешно" + }, "removePasskey": { "message": "Удалить passkey" }, "passkeyRemoved": { "message": "Passkey удален" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" и доступны только через консоль администратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года не назначенные элементы организации больше не будут видны в представлении \"Все хранилища\" и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "чтобы сделать их видимыми.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консоли администратора" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index fb026226bb..f225e854aa 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -3,12 +3,12 @@ "message": "බිට්වාඩන්" }, "extName": { - "message": "බිට්වාඩන් - නොමිලේ මුරපදය කළමනාකරු", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "ඔබගේ සියලු උපාංග සඳහා ආරක්ෂිත සහ නොමිලේ මුරපද කළමණාකරුවෙකු.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ඔබගේ ආරක්ෂිත සුරක්ෂිතාගාරය වෙත පිවිසීමට හෝ නව ගිණුමක් නිර්මාණය කරන්න." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "ප්රධාන මුරපදය වෙනස්" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "ඇඟිලි සලකුණු වාක්ය ඛණ්ඩය", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "එකතු කරන ලද ෆෝල්ඩරය" }, - "changeMasterPass": { - "message": "ප්රධාන මුරපදය වෙනස්" - }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com වෙබ් සුරක්ෂිතාගාරයේ ඔබේ ප්රධාන මුරපදය වෙනස් කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" - }, "twoStepLoginConfirmation": { "message": "ආරක්ෂක යතුරක්, සත්යාපන යෙදුම, කෙටි පණිවුඩ, දුරකථන ඇමතුමක් හෝ විද්යුත් තැපෑල වැනි වෙනත් උපාංගයක් සමඟ ඔබේ පිවිසුම සත්යාපනය කිරීමට ඔබට අවශ්ය වීමෙන් ද්වි-පියවර පිවිසුම ඔබගේ ගිණුම වඩාත් සුරක්ෂිත කරයි. බිට්වොන්.com වෙබ් සුරක්ෂිතාගාරයේ ද්වි-පියවර පිවිසුම සක්රීය කළ හැකිය. ඔබට දැන් වෙබ් අඩවියට පිවිසීමට අවශ්යද?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "සුරක්ෂිතාගාරය ලොක් කරන්න" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "අභිරුචි ක්ෂේත්ර" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index a7948d78f3..7bcffc04ac 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Bezplatný správca hesiel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden je bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prihláste sa, alebo vytvorte nový účet pre prístup k vášmu bezpečnému trezoru." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Zmeniť hlavné heslo" }, + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." + }, "fingerprintPhrase": { "message": "Fráza odtlačku", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Pridaný priečinok" }, - "changeMasterPass": { - "message": "Zmeniť hlavné heslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Teraz si môžete zmeniť svoje hlavné heslo vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" - }, "twoStepLoginConfirmation": { "message": "Dvojstupňové prihlasovanie robí váš účet bezpečnejším vďaka vyžadovaniu bezpečnostného kódu z overovacej aplikácie vždy, keď sa prihlásite. Dvojstupňové prihlasovanie môžete povoliť vo webovom trezore bitwarden.com. Chcete navštíviť túto stránku teraz?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zamknúť trezor" }, - "privateModeWarning": { - "message": "Podpora privátneho režimu je experimentálna a niektoré funkcie sú obmedzené." - }, "customFields": { "message": "Vlastné polia" }, @@ -1754,7 +1751,7 @@ } }, "send": { - "message": "Odoslať", + "message": "Send", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "searchSends": { @@ -3000,16 +2997,36 @@ "message": "Chyba pri ukladaní prihlasovacích údajov. Viac informácii nájdete v konzole.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Úspech" + }, "removePasskey": { "message": "Odstrániť prístupový kľúč" }, "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" }, - "unassignedItemsBanner": { - "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky Trezory a sú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." }, - "unassignedItemsBannerSelfHost": { - "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby boli viditeľné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Správcovská konzola" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index 2fac491c9c..170ee146f7 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Brezplačni upravitelj gesel", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Varen in brezplačen upravitelj gesel za vse vaše naprave.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Prijavite se ali ustvarite nov račun za dostop do svojega varnega trezorja." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Spremeni glavno geslo" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Mapa dodana" }, - "changeMasterPass": { - "message": "Spremeni glavno geslo" - }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v Bitwardnovem spletnem trezorju. Želite zdaj obiskati Bitwardnovo spletno stran?" - }, "twoStepLoginConfirmation": { "message": "Avtentikacija v dveh korakih dodatno varuje vaš račun, saj zahteva, da vsakokratno prijavo potrdite z drugo napravo, kot je varnostni ključ, aplikacija za preverjanje pristnosti, SMS, telefonski klic ali e-pošta. Avtentikacijo v dveh korakih lahko omogočite v spletnem trezorju bitwarden.com. Ali želite spletno stran obiskati sedaj?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Zakleni trezor" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Polja po meri" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 6ec1b6181b..3827a9dedd 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - бесплатни менаџер лозинки", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Сигурни и бесплатни менаџер лозинки за све ваше уређаје.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Пријавите се или креирајте нови налог за приступ сефу." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Промени главну лозинку" }, + "continueToWebApp": { + "message": "Ићи на веб апликацију?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." + }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Фасцикла додата" }, - "changeMasterPass": { - "message": "Промени главну лозинку" - }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" - }, "twoStepLoginConfirmation": { "message": "Пријава у два корака чини ваш налог сигурнијим захтевом да верификујете своје податке помоћу другог уређаја, као што су безбедносни кључ, апликација, СМС-а, телефонски позив или имејл. Пријављивање у два корака може се омогућити на веб сефу. Да ли желите да посетите веб страницу сада?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Закључај сеф" }, - "privateModeWarning": { - "message": "Подршка за приватни режим је експериментална и неке функције су ограничене." - }, "customFields": { "message": "Прилагођена Поља" }, @@ -3000,16 +2997,36 @@ "message": "Грешка при чувању акредитива. Проверите конзолу за детаље.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успех" + }, "removePasskey": { "message": "Уклонити приступачни кључ" }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "unassignedItemsBannerNotice": { + "message": "Напомена: Недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." }, - "unassignedItemsBannerSelfHost": { - "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у приказу Сви сефови и биће доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "unassignedItemsBannerSelfHostNotice": { + "message": "Напомена: од 16 Маја 2024м недодељене ставке организације више нису видљиве у приказу Сви сефови и доступне су само преко Админ конзоле." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Администраторска конзола" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при додељивању циљне колекције." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index d798b98ea0..0939ac0280 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Gratis lösenordshanterare", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden är en säker och gratis lösenordshanterare för alla dina enheter.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Logga in eller skapa ett nytt konto för att komma åt ditt säkra valv." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ändra huvudlösenord" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Lade till mapp" }, - "changeMasterPass": { - "message": "Ändra huvudlösenord" - }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord på bitwardens webbvalv. Vill du besöka webbplatsen nu?" - }, "twoStepLoginConfirmation": { "message": "Tvåstegsverifiering gör ditt konto säkrare genom att kräva att du verifierar din inloggning med en annan enhet, t.ex. en säkerhetsnyckel, autentiseringsapp, SMS, telefonsamtal eller e-post. Tvåstegsverifiering kan aktiveras i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lås valvet" }, - "privateModeWarning": { - "message": "Stöd för privat läge är experimentellt och vissa funktioner är begränsade." - }, "customFields": { "message": "Anpassade fält" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index 023e03b834..fec6ab713c 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "A secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Log in or create a new account to access your secure vault." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change master password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Folder added" }, - "changeMasterPass": { - "message": "Change master password" - }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to verify your login with another device such as a security key, authenticator app, SMS, phone call, or email. Two-step login can be set up on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Lock the vault" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 827ca72854..009685b14b 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -3,12 +3,12 @@ "message": "bitwarden" }, "extName": { - "message": "bitwarden - Free Password Manager", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "bitwarden is a secure and free password manager for all of your devices.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "ล็อกอิน หรือ สร้างบัญชีใหม่ เพื่อใช้งานตู้นิรภัยของคุณ" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Change Master Password" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "เพิ่มโฟลเดอร์แล้ว" }, - "changeMasterPass": { - "message": "Change Master Password" - }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักได้ที่เว็บตู้เซฟ bitwarden.com คุณต้องการเปิดเว็บไซต์เลยหรือไม่?" - }, "twoStepLoginConfirmation": { "message": "Two-step login makes your account more secure by requiring you to enter a security code from an authenticator app whenever you log in. Two-step login can be enabled on the bitwarden.com web vault. Do you want to visit the website now?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "ล็อกตู้เซฟ" }, - "privateModeWarning": { - "message": "Private mode support is experimental and some features are limited." - }, "customFields": { "message": "Custom Fields" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index cd0e12e6b0..866125dbec 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Ücretsiz Parola Yöneticisi", + "message": "Bitwarden Parola Yöneticisi", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Tüm cihazlarınız için güvenli ve ücretsiz bir parola yöneticisi.", - "description": "Extension description" + "message": "Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Güvenli kasanıza ulaşmak için giriş yapın veya yeni bir hesap oluşturun." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Ana parolayı değiştir" }, + "continueToWebApp": { + "message": "Web uygulamasına devam edilsin mi?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolanızı Bitwarden web uygulamasında değiştirebilirsiniz." + }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -327,7 +333,7 @@ "message": "Parola" }, "totp": { - "message": "Authenticator secret" + "message": "Kimlik doğrulama sırrı" }, "passphrase": { "message": "Uzun söz" @@ -522,16 +528,16 @@ "message": "Seçilen hesap bu sayfada otomatik olarak doldurulamadı. Lütfen bilgileri elle kopyalayıp yapıştırın." }, "totpCaptureError": { - "message": "Mevcut web sayfasından QR kodu taranamıyor" + "message": "Mevcut web sayfasındaki QR kodu taranamıyor" }, "totpCaptureSuccess": { "message": "Kimlik doğrulama anahtarı eklendi" }, "totpCapture": { - "message": "Mevcut web sayfasından kimlik doğrulayıcı QR kodunu tarayın" + "message": "Mevcut web sayfasındaki kimlik doğrulayıcı QR kodunu tarayın" }, "copyTOTP": { - "message": "Kimlik Doğrulayıcı anahtarını kopyala (TOTP)" + "message": "Kimlik doğrulama anahtarını kopyala (TOTP)" }, "loggedOut": { "message": "Çıkış yapıldı" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Klasör eklendi" }, - "changeMasterPass": { - "message": "Ana parolayı değiştir" - }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteye gitmek ister misiniz?" - }, "twoStepLoginConfirmation": { "message": "İki aşamalı giriş, hesabınıza girererken işlemi bir güvenlik anahtarı, şifrematik uygulaması, SMS, telefon araması veya e-posta gibi ek bir yöntemle doğrulamanızı isteyerek hesabınızın güvenliğini artırır. İki aşamalı giriş özelliğini bitwarden.com web kasası üzerinden etkinleştirebilirsiniz. Şimdi siteye gitmek ister misiniz?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Kasayı kilitle" }, - "privateModeWarning": { - "message": "Gizli mod desteği deneyseldir ve bazı özellikler kısıtlıdır." - }, "customFields": { "message": "Özel alanlar" }, @@ -2005,7 +2002,7 @@ "message": "Klasör seç..." }, "noFoldersFound": { - "message": "Herhangi bir klasör bulunamadı", + "message": "Hiçbir klasör bulunamadı", "description": "Used as a message within the notification bar when no folders are found" }, "orgPermissionsUpdatedMustSetPassword": { @@ -2652,13 +2649,13 @@ } }, "tryAgain": { - "message": "Tekrar deneyin" + "message": "Yeniden dene" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN ayarlayın." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "PIN Belirle" + "message": "PIN belirle" }, "verifyWithBiometrics": { "message": "Biyometri ile doğrula" @@ -2673,7 +2670,7 @@ "message": "Farklı bir yönteme mi ihtiyacınız var?" }, "useMasterPassword": { - "message": "Ana parolayı kullanın" + "message": "Ana parolayı kullan" }, "usePin": { "message": "PIN kullan" @@ -2685,7 +2682,7 @@ "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { - "message": "Kodu tekrar gönder" + "message": "Kodu yeniden gönder" }, "total": { "message": "Toplam" @@ -2700,19 +2697,19 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "DUO'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." }, "duoRequiredForAccount": { - "message": "Hesabınız için Duo'ya iki adımlı giriş yapmanız gerekiyor." + "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "popoutTheExtensionToCompleteLogin": { - "message": "Oturum açma işlemini tamamlamak için uzantıyı açın." + "message": "Giriş işlemini tamamlamak için uzantıyı dışarı alın." }, "popoutExtension": { - "message": "Popout uzantısı" + "message": "Uzantıyı dışarı al" }, "launchDuo": { - "message": "DUO'yu başlat" + "message": "Duo'yu başlat" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -2795,7 +2792,7 @@ "message": "Geçiş anahtarı klonlanan öğeye kopyalanmayacaktır. Bu öğeyi klonlamaya devam etmek istiyor musunuz?" }, "passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": { - "message": "Açılan sitenin gerektirdiği doğrulama. Bu özellik henüz ana şifresi olmayan hesaplara uygulanmamaktadır." + "message": "Site kimlik doğrulaması gerektiriyor. Bu özellik henüz ana parolası olmayan hesaplarda kullanılamaz." }, "logInWithPasskey": { "message": "Geçiş anahtarı ile giriş yapılsın mı?" @@ -2828,13 +2825,13 @@ "message": "Geçiş anahtarının üzerine yazılsın mı?" }, "overwritePasskeyAlert": { - "message": "Bu öğe zaten bir şifre anahtarı içeriyor. Geçerli şifrenin üzerine yazmak istediğinizden emin misiniz?" + "message": "Bu kayıt zaten bir geçiş anahtarı içeriyor. Mevcut geçiş anahtarının üzerine yazmak istediğinizden emin misiniz?" }, "featureNotSupported": { "message": "Bu özellik henüz desteklenmiyor" }, "yourPasskeyIsLocked": { - "message": "Şifreyi kullanmak için kimlik doğrulama gerekiyor. Devam etmek için kimliğinizi doğrulayın." + "message": "Geçiş anahtarını kullanmak için kimlik doğrulama gerekiyor. Devam etmek için kimliğinizi doğrulayın." }, "multifactorAuthenticationCancelled": { "message": "Çok faktörlü kimlik doğrulama iptal edildi" @@ -2943,10 +2940,10 @@ "message": "konum" }, "useDeviceOrHardwareKey": { - "message": "Cihazınızı veya donanım anahtarınızı kullanın" + "message": "Cihazınızı veya donanımsal anahtarınızı kullanın" }, "justOnce": { - "message": "Yalnızca bir kez" + "message": "Yalnızca bir defa" }, "alwaysForThisSite": { "message": "Bu site için her zaman" @@ -2961,23 +2958,23 @@ } }, "commonImportFormats": { - "message": "Ortak formatlar", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, "overrideDefaultBrowserAutofillTitle": { - "message": "Bitwarden varsayılan şifre yöneticiniz yapılsın mı?", + "message": "Bitwarden varsayılan parola yöneticiniz yapılsın mı?", "description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutofillDescription": { - "message": "Bu seçeneğin göz ardı edilmesi, Bitwarden otomatik doldurma menüsü ile tarayıcınızınki arasında çakışmalara neden olabilir.", + "message": "Bu seçeneği göz ardı ederseniz Bitwarden otomatik doldurma menüsüyle tarayıcınızınki arasında çakışma yaşanabilir.", "description": "Dialog message facilitating the ability to override a chrome browser's default autofill behavior" }, "overrideDefaultBrowserAutoFillSettings": { - "message": "Bitwarden'ı varsayılan şifre yöneticiniz yapın", + "message": "Bitwarden'ı varsayılan parola yöneticiniz yapın", "description": "Label for the setting that allows overriding the default browser autofill settings" }, "privacyPermissionAdditionNotGrantedTitle": { - "message": "Bitwarden varsayılan parola yöneticisi olarak ayarlanamıyor", + "message": "Bitwarden varsayılan parola yöneticisi olarak ayarlanamadı", "description": "Title for the dialog that appears when the user has not granted the extension permission to set privacy settings" }, "privacyPermissionAdditionNotGrantedDescription": { @@ -3000,16 +2997,36 @@ "message": "Kimlik bilgileri kaydedilirken hata oluştu. Ayrıntılar için konsolu kontrol edin.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Başarılı" + }, "removePasskey": { - "message": "Remove passkey" + "message": "Geçiş anahtarını kaldır" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Geçiş anahtarı kaldırıldı" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Yönetici Konsolu" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 4820860de2..919066188a 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden - це захищений і безкоштовний менеджер паролів для всіх ваших пристроїв.", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Для доступу до сховища увійдіть в обліковий запис, або створіть новий." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Змінити головний пароль" }, + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." + }, "fingerprintPhrase": { "message": "Фраза відбитка", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "Теку додано" }, - "changeMasterPass": { - "message": "Змінити головний пароль" - }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" - }, "twoStepLoginConfirmation": { "message": "Двоетапна перевірка дає змогу надійніше захистити ваш обліковий запис, вимагаючи підтвердження входу з використанням іншого пристрою, наприклад, за допомогою ключа безпеки, програми автентифікації, SMS, телефонного виклику, або е-пошти. Ви можете налаштувати двоетапну перевірку в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Заблокувати сховище" }, - "privateModeWarning": { - "message": "Приватний режим - це експериментальна функція і деякі можливості обмежені." - }, "customFields": { "message": "Власні поля" }, @@ -3000,16 +2997,36 @@ "message": "Помилка збереження облікових даних. Перегляньте подробиці в консолі.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Успішно" + }, "removePasskey": { "message": "Вилучити ключ доступу" }, "passkeyRemoved": { "message": "Ключ доступу вилучено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" і доступні лише в консолі адміністратора." }, - "unassignedItemsBannerSelfHost": { - "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі в поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі у поданні \"Усі сховища\" і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "щоб зробити їх видимими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "консолі адміністратора," + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 234e60e756..f7a0d50bd5 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - Quản lý mật khẩu miễn phí", + "message": "Bitwarden - Trình Quản lý Mật khẩu", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn.", - "description": "Extension description" + "message": "Ở nhà, ở cơ quan, hay trên đường đi, Bitwarden sẽ bảo mật tất cả mật khẩu, passkey, và thông tin cá nhân của bạn", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "Đăng nhập hoặc tạo tài khoản mới để truy cập kho lưu trữ của bạn." @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "Thay đổi mật khẩu chính" }, + "continueToWebApp": { + "message": "Tiếp tục tới ứng dụng web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bạn có thể thay đổi mật khẩu chính của mình trên Bitwarden bản web." + }, "fingerprintPhrase": { "message": "Fingerprint Phrase", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -415,7 +421,7 @@ "message": "Khóa ngay" }, "lockAll": { - "message": "Lock all" + "message": "Khóa tất cả" }, "immediately": { "message": "Ngay lập tức" @@ -494,10 +500,10 @@ "message": "Tài khoản mới của bạn đã được tạo! Bạn có thể đăng nhập từ bây giờ." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Bạn đã đăng nhập thành công" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Bạn có thể đóng cửa sổ này" }, "masterPassSent": { "message": "Chúng tôi đã gửi cho bạn email có chứa gợi ý mật khẩu chính của bạn." @@ -522,16 +528,16 @@ "message": "Không thể tự động điền mục đã chọn trên trang này. Hãy thực hiện sao chép và dán thông tin một cách thủ công." }, "totpCaptureError": { - "message": "Unable to scan QR code from the current webpage" + "message": "Không thể quét mã QR từ trang web hiện tại" }, "totpCaptureSuccess": { - "message": "Authenticator key added" + "message": "Đã thêm khóa xác thực" }, "totpCapture": { - "message": "Scan authenticator QR code from current webpage" + "message": "Quét mã QR xác thực từ trang web hiện tại" }, "copyTOTP": { - "message": "Copy Authenticator key (TOTP)" + "message": "Sao chép khóa Authenticator (TOTP)" }, "loggedOut": { "message": "Đã đăng xuất" @@ -557,12 +563,6 @@ "addedFolder": { "message": "Đã thêm thư mục" }, - "changeMasterPass": { - "message": "Thay đổi mật khẩu chính" - }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong trang web kho lưu trữ của Bitwarden. Bạn có muốn truy cập trang web ngay bây giờ không?" - }, "twoStepLoginConfirmation": { "message": "Xác thực hai lớp giúp cho tài khoản của bạn an toàn hơn bằng cách yêu cầu bạn xác minh thông tin đăng nhập của bạn bằng một thiết bị khác như khóa bảo mật, ứng dụng xác thực, SMS, cuộc gọi điện thoại hoặc email. Bạn có thể bật xác thực hai lớp trong kho bitwarden nền web. Bạn có muốn ghé thăm trang web bây giờ?" }, @@ -650,7 +650,7 @@ "message": "'Thông báo Thêm đăng nhập' sẽ tự động nhắc bạn lưu các đăng nhập mới vào hầm an toàn của bạn bất cứ khi nào bạn đăng nhập trang web lần đầu tiên." }, "addLoginNotificationDescAlt": { - "message": "Ask to add an item if one isn't found in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để thêm một mục nếu không tìm thấy mục đó trong hòm của bạn. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "showCardsCurrentTab": { "message": "Hiển thị thẻ trên trang Tab" @@ -685,13 +685,13 @@ "message": "Yêu cầu cập nhật mật khẩu đăng nhập khi phát hiện thay đổi trên trang web." }, "changedPasswordNotificationDescAlt": { - "message": "Ask to update a login's password when a change is detected on a website. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để cập nhật mật khẩu khi phát hiện có sự thay đổi trên trang web. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableUsePasskeys": { - "message": "Ask to save and use passkeys" + "message": "Đưa ra lựa chọn để lưu và sử dụng passkey" }, "usePasskeysDesc": { - "message": "Ask to save new passkeys or log in with passkeys stored in your vault. Applies to all logged in accounts." + "message": "Đưa ra lựa chọn để lưu passkey mới hoặc đăng nhập bằng passkey đã lưu trong hòm. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "notificationChangeDesc": { "message": "Bạn có muốn cập nhật mật khẩu này trên Bitwarden không?" @@ -712,7 +712,7 @@ "message": "Sử dụng một đúp chuột để truy cập vào việc tạo mật khẩu và thông tin đăng nhập phù hợp cho trang web. " }, "contextMenuItemDescAlt": { - "message": "Use a secondary click to access password generation and matching logins for the website. Applies to all logged in accounts." + "message": "Truy cập trình khởi tạo mật khẩu và các mục đăng nhập đã lưu của trang web bằng cách nhấn đúp chuột. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "defaultUriMatchDetection": { "message": "Phương thức kiểm tra URI mặc định", @@ -728,7 +728,7 @@ "message": "Thay đổi màu sắc ứng dụng." }, "themeDescAlt": { - "message": "Change the application's color theme. Applies to all logged in accounts." + "message": "Thay đổi tông màu giao diện của ứng dụng. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "dark": { "message": "Tối", @@ -1061,10 +1061,10 @@ "message": "Tắt cài đặt trình quản lý mật khẩu tích hợp trong trình duyệt của bạn để tránh xung đột." }, "turnOffBrowserBuiltInPasswordManagerSettingsLink": { - "message": "Edit browser settings." + "message": "Thay đổi cài đặt của trình duyệt." }, "autofillOverlayVisibilityOff": { - "message": "Off", + "message": "Tắt", "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "Khoá kho lưu trữ" }, - "privateModeWarning": { - "message": "Hỗ trợ cho chế độ riêng tư đang được thử nghiệm và hạn chế một số tính năng." - }, "customFields": { "message": "Trường tùy chỉnh" }, @@ -1168,7 +1165,7 @@ "message": "Hiển thị một ảnh nhận dạng bên cạnh mỗi lần đăng nhập." }, "faviconDescAlt": { - "message": "Show a recognizable image next to each login. Applies to all logged in accounts." + "message": "Hiển thị một biểu tượng dễ nhận dạng bên cạnh mỗi mục đăng nhập. Áp dụng với mọi tài khoản đăng nhập trên thiết bị." }, "enableBadgeCounter": { "message": "Hiển thị biểu tượng bộ đếm" @@ -1500,7 +1497,7 @@ "message": "Mã PIN không hợp lệ." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Mã PIN bị gõ sai quá nhiều lần. Đang đăng xuất." }, "unlockWithBiometrics": { "message": "Mở khóa bằng sinh trắc học" @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Lưu ý: Các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Lưu ý: Vào ngày 16 tháng 5 năm 2024, các mục tổ chức chưa được chỉ định sẽ không còn hiển thị trong chế độ xem Tất cả Vault và sẽ chỉ có thể truy cập được qua Bảng điều khiển dành cho quản trị viên." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Gán các mục này vào một bộ sưu tập từ", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "để làm cho chúng hiển thị.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Bảng điều khiển dành cho quản trị viên" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 519313df81..706bf7a851 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免费密码管理器", + "message": "Bitwarden 密码管理器", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "安全且免费的跨平台密码管理器。", - "description": "Extension description" + "message": "无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登录或者创建一个账户来访问您的安全密码库。" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "更改主密码" }, + "continueToWebApp": { + "message": "前往网页 App 吗?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" + }, "fingerprintPhrase": { "message": "指纹短语", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "文件夹已添加" }, - "changeMasterPass": { - "message": "修改主密码" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页版密码库修改主密码。您现在要访问这个网站吗?" - }, "twoStepLoginConfirmation": { "message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件)来验证您的登录,这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "锁定密码库" }, - "privateModeWarning": { - "message": "私密模式的支持是实验性的,某些功能会受到限制。" - }, "customFields": { "message": "自定义字段" }, @@ -3000,16 +2997,36 @@ "message": "保存凭据时出错。检查控制台以获取详细信息。", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "成功" + }, "removePasskey": { "message": "移除通行密钥" }, "passkeyRemoved": { "message": "通行密钥已移除" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "unassignedItemsBannerNotice": { + "message": "注意:未分配的组织项目在「所有密码库」视图中不再可见,只能通过管理控制台访问。" }, - "unassignedItemsBannerSelfHost": { - "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "unassignedItemsBannerSelfHostNotice": { + "message": "注意:从 2024 年 5 月 16 日起,未分配的组织项目在「所有密码库」视图中将不再可见,只能通过管理控制台访问。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "将这些项目分配到集合,通过", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ",以使其可见。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "管理控制台" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index b6f1ff574a..9300289795 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -3,12 +3,12 @@ "message": "Bitwarden" }, "extName": { - "message": "Bitwarden - 免費密碼管理工具", + "message": "Bitwarden Password Manager", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { - "message": "Bitwarden 是一款安全、免費、跨平台的密碼管理工具。", - "description": "Extension description" + "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information", + "description": "Extension description, MUST be less than 112 characters (Safari restriction)" }, "loginOrCreateNewAccount": { "message": "登入或建立帳戶以存取您的安全密碼庫。" @@ -172,6 +172,12 @@ "changeMasterPassword": { "message": "變更主密碼" }, + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." + }, "fingerprintPhrase": { "message": "指紋短語", "description": "A 'fingerprint phrase' is a unique word phrase (similar to a passphrase) that a user can use to authenticate their public key with another user, for the purposes of sharing." @@ -557,12 +563,6 @@ "addedFolder": { "message": "資料夾已新增" }, - "changeMasterPass": { - "message": "變更主密碼" - }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" - }, "twoStepLoginConfirmation": { "message": "兩步驟登入需要您從其他裝置(例如安全鑰匙、驗證器程式、SMS、手機或電子郵件)來驗證您的登入,這使您的帳戶更加安全。兩步驟登入可以在 bitwarden.com 網頁版密碼庫啟用。現在要前往嗎?" }, @@ -1120,9 +1120,6 @@ "commandLockVaultDesc": { "message": "鎖定密碼庫" }, - "privateModeWarning": { - "message": "私密模式的支援是實驗性功能,部分功能無法完全發揮作用。" - }, "customFields": { "message": "自訂欄位" }, @@ -3000,16 +2997,36 @@ "message": "Error saving credentials. Check console for details.", "description": "Notification message for when saving credentials has failed." }, + "success": { + "message": "Success" + }, "removePasskey": { "message": "Remove passkey" }, "passkeyRemoved": { "message": "Passkey removed" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in the All Vaults view and only accessible via the Admin Console." }, - "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in the All Vaults view and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "adminConsole": { + "message": "Admin Console" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts similarity index 78% rename from apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts rename to apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts index cac6f9bbe8..42a8232c3e 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-service.factory.ts @@ -1,5 +1,5 @@ -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceInitOptions, @@ -34,6 +34,7 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "../../../platform/background/service-factories/key-generation-service.factory"; +import { logServiceFactory } from "../../../platform/background/service-factories/log-service.factory"; import { PlatformUtilsServiceInitOptions, platformUtilsServiceFactory, @@ -52,9 +53,9 @@ import { userDecryptionOptionsServiceFactory, } from "./user-decryption-options-service.factory"; -type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; +type DeviceTrustServiceFactoryOptions = FactoryOptions; -export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & +export type DeviceTrustServiceInitOptions = DeviceTrustServiceFactoryOptions & KeyGenerationServiceInitOptions & CryptoFunctionServiceInitOptions & CryptoServiceInitOptions & @@ -67,16 +68,16 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor SecureStorageServiceInitOptions & UserDecryptionOptionsServiceInitOptions; -export function deviceTrustCryptoServiceFactory( - cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, - opts: DeviceTrustCryptoServiceInitOptions, -): Promise { +export function deviceTrustServiceFactory( + cache: { deviceTrustService?: DeviceTrustServiceAbstraction } & CachedServices, + opts: DeviceTrustServiceInitOptions, +): Promise { return factory( cache, - "deviceTrustCryptoService", + "deviceTrustService", opts, async () => - new DeviceTrustCryptoService( + new DeviceTrustService( await keyGenerationServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), @@ -88,6 +89,7 @@ export function deviceTrustCryptoServiceFactory( await stateProviderFactory(cache, opts), await secureStorageServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts new file mode 100644 index 0000000000..eb5ba3a264 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/kdf-config-service.factory.ts @@ -0,0 +1,28 @@ +import { KdfConfigService as AbstractKdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; + +import { + FactoryOptions, + CachedServices, + factory, +} from "../../../platform/background/service-factories/factory-options"; +import { + StateProviderInitOptions, + stateProviderFactory, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type KdfConfigServiceFactoryOptions = FactoryOptions; + +export type KdfConfigServiceInitOptions = KdfConfigServiceFactoryOptions & StateProviderInitOptions; + +export function kdfConfigServiceFactory( + cache: { kdfConfigService?: AbstractKdfConfigService } & CachedServices, + opts: KdfConfigServiceInitOptions, +): Promise { + return factory( + cache, + "kdfConfigService", + opts, + async () => new KdfConfigService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index f184072cce..c414300431 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -65,9 +65,10 @@ import { AuthRequestServiceInitOptions, } from "./auth-request-service.factory"; import { - deviceTrustCryptoServiceFactory, - DeviceTrustCryptoServiceInitOptions, -} from "./device-trust-crypto-service.factory"; + deviceTrustServiceFactory, + DeviceTrustServiceInitOptions, +} from "./device-trust-service.factory"; +import { kdfConfigServiceFactory, KdfConfigServiceInitOptions } from "./kdf-config-service.factory"; import { keyConnectorServiceFactory, KeyConnectorServiceInitOptions, @@ -102,11 +103,12 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions EncryptServiceInitOptions & PolicyServiceInitOptions & PasswordStrengthServiceInitOptions & - DeviceTrustCryptoServiceInitOptions & + DeviceTrustServiceInitOptions & AuthRequestServiceInitOptions & UserDecryptionOptionsServiceInitOptions & GlobalStateProviderInitOptions & - BillingAccountProfileStateServiceInitOptions; + BillingAccountProfileStateServiceInitOptions & + KdfConfigServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -135,11 +137,12 @@ export function loginStrategyServiceFactory( await encryptServiceFactory(cache, opts), await passwordStrengthServiceFactory(cache, opts), await policyServiceFactory(cache, opts), - await deviceTrustCryptoServiceFactory(cache, opts), + await deviceTrustServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts index f5360f48fa..db16245f67 100644 --- a/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/pin-crypto-service.factory.ts @@ -22,13 +22,16 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; + type PinCryptoServiceFactoryOptions = FactoryOptions; export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions & StateServiceInitOptions & CryptoServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - LogServiceInitOptions; + LogServiceInitOptions & + KdfConfigServiceInitOptions; export function pinCryptoServiceFactory( cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices, @@ -44,6 +47,7 @@ export function pinCryptoServiceFactory( await cryptoServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await logServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts index 1d79bbbaf1..5af5eb0017 100644 --- a/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/two-factor-service.factory.ts @@ -1,11 +1,13 @@ import { TwoFactorService as AbstractTwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; +import { GlobalStateProvider } from "@bitwarden/common/platform/state"; import { FactoryOptions, CachedServices, factory, } from "../../../platform/background/service-factories/factory-options"; +import { globalStateProviderFactory } from "../../../platform/background/service-factories/global-state-provider.factory"; import { I18nServiceInitOptions, i18nServiceFactory, @@ -19,7 +21,8 @@ type TwoFactorServiceFactoryOptions = FactoryOptions; export type TwoFactorServiceInitOptions = TwoFactorServiceFactoryOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + GlobalStateProvider; export async function twoFactorServiceFactory( cache: { twoFactorService?: AbstractTwoFactorService } & CachedServices, @@ -33,6 +36,7 @@ export async function twoFactorServiceFactory( new TwoFactorService( await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await globalStateProviderFactory(cache, opts), ), ); service.init(); diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index a8b67b21ca..d6f9ce7624 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -32,6 +32,7 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { accountServiceFactory, AccountServiceInitOptions } from "./account-service.factory"; +import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -59,7 +60,8 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + KdfConfigServiceInitOptions; export function userVerificationServiceFactory( cache: { userVerificationService?: AbstractUserVerificationService } & CachedServices, @@ -82,6 +84,7 @@ export function userVerificationServiceFactory( await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html index aebf2219ff..806dae084d 100644 --- a/apps/browser/src/auth/popup/account-switching/account-switcher.component.html +++ b/apps/browser/src/auth/popup/account-switching/account-switcher.component.html @@ -49,7 +49,7 @@

- {{ biometricError }}

{{ "awaitDesktop" | i18n }} diff --git a/apps/browser/src/auth/popup/lock.component.ts b/apps/browser/src/auth/popup/lock.component.ts index 16c32337cf..782e37b864 100644 --- a/apps/browser/src/auth/popup/lock.component.ts +++ b/apps/browser/src/auth/popup/lock.component.ts @@ -11,7 +11,8 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -58,14 +59,15 @@ export class LockComponent extends BaseLockComponent { policyApiService: PolicyApiServiceAbstraction, policyService: InternalPolicyService, passwordStrengthService: PasswordStrengthServiceAbstraction, - private authService: AuthService, + authService: AuthService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, private routerService: BrowserRouterService, biometricStateService: BiometricStateService, accountService: AccountService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -85,11 +87,13 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, accountService, + authService, + kdfConfigService, ); this.successRoute = "/tabs/current"; this.isInitialLockScreen = (window as any).previousPopupUrl == null; @@ -139,15 +143,17 @@ export class LockComponent extends BaseLockComponent { try { success = await super.unlockBiometric(); } catch (e) { - const error = BiometricErrors[e as BiometricErrorTypes]; + const error = BiometricErrors[e?.message as BiometricErrorTypes]; if (error == null) { this.logService.error("Unknown error: " + e); + return false; } this.biometricError = this.i18nService.t(error.description); + } finally { + this.pendingBiometric = false; } - this.pendingBiometric = false; return success; } diff --git a/apps/browser/src/auth/popup/login-via-auth-request.component.ts b/apps/browser/src/auth/popup/login-via-auth-request.component.ts index 52f311ce7b..158296058e 100644 --- a/apps/browser/src/auth/popup/login-via-auth-request.component.ts +++ b/apps/browser/src/auth/popup/login-via-auth-request.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -47,7 +47,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, syncService: SyncService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -69,7 +69,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index b24a25a0f1..7a4211a5cc 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -57,7 +57,6 @@ -

+

+ {{ "accountSecurity" | i18n }} +

+
+ +
+ +
+
+

{{ "unlockMethods" | i18n }}

+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+

{{ "sessionTimeoutHeader" | i18n }}

+
+ + + {{ + "vaultTimeoutPolicyWithActionInEffect" + | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) + }} + + + {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} + + + {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} + + + + +
+ + +
+ +
+
+
+

{{ "otherOptions" | i18n }}

+
+ + + + + +
+
+
diff --git a/apps/browser/src/popup/settings/settings.component.ts b/apps/browser/src/auth/popup/settings/account-security.component.ts similarity index 82% rename from apps/browser/src/popup/settings/settings.component.ts rename to apps/browser/src/auth/popup/settings/account-security.component.ts index fa6c64fcc5..88365e7b47 100644 --- a/apps/browser/src/popup/settings/settings.component.ts +++ b/apps/browser/src/auth/popup/settings/account-security.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectorRef, Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; -import { Router } from "@angular/router"; import { BehaviorSubject, combineLatest, @@ -21,8 +20,8 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaul import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; -import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -33,35 +32,20 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { DialogService } from "@bitwarden/components"; -import { SetPinComponent } from "../../auth/popup/components/set-pin.component"; -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 { 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 { SetPinComponent } from "../components/set-pin.component"; -import { AboutComponent } from "./about.component"; import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component"; -const RateUrls = { - [DeviceType.ChromeExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.FirefoxExtension]: - "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", - [DeviceType.OperaExtension]: - "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", - [DeviceType.EdgeExtension]: - "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", - [DeviceType.VivaldiExtension]: - "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", - [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", -}; - @Component({ - selector: "app-settings", - templateUrl: "settings.component.html", + selector: "auth-account-security", + templateUrl: "account-security.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class SettingsComponent implements OnInit { +export class AccountSecurityComponent implements OnInit { protected readonly VaultTimeoutAction = VaultTimeoutAction; availableVaultTimeoutActions: VaultTimeoutAction[] = []; @@ -86,6 +70,7 @@ export class SettingsComponent implements OnInit { private destroy$ = new Subject(); constructor( + private accountService: AccountService, private policyService: PolicyService, private formBuilder: FormBuilder, private platformUtilsService: PlatformUtilsService, @@ -93,7 +78,6 @@ export class SettingsComponent implements OnInit { private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, public messagingService: MessagingService, - private router: Router, private environmentService: EnvironmentService, private cryptoService: CryptoService, private stateService: StateService, @@ -423,22 +407,6 @@ export class SettingsComponent implements OnInit { ); } - async lock() { - await this.vaultTimeoutService.lock(); - } - - async logOut() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "logOut" }, - content: { key: "logOutConfirmation" }, - type: "info", - }); - - if (confirmed) { - this.messagingService.send("logout"); - } - } - async changePassword() { const confirmed = await this.dialogService.openSimpleDialog({ title: { key: "continueToWebApp" }, @@ -465,44 +433,6 @@ export class SettingsComponent implements OnInit { } } - async share() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "learnOrg" }, - content: { key: "learnOrgConfirmation" }, - type: "info", - }); - if (confirmed) { - // 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/about-organizations/"); - } - } - - async webVault() { - const env = await firstValueFrom(this.environmentService.environment$); - const url = env.getWebVaultUrl(); - await BrowserApi.createNewTab(url); - } - - async import() { - await this.router.navigate(["/import"]); - if (await BrowserApi.isPopupOpen()) { - // 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 - BrowserPopupUtils.openCurrentPagePopout(window); - } - } - - export() { - // 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(["/export"]); - } - - about() { - this.dialogService.open(AboutComponent); - } - async fingerprint() { const fingerprint = await this.cryptoService.getFingerprint( await this.stateService.getUserId(), @@ -515,11 +445,21 @@ export class SettingsComponent implements OnInit { return firstValueFrom(dialogRef.closed); } - rate() { - const deviceType = this.platformUtilsService.getDevice(); - // 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((RateUrls as any)[deviceType]); + async lock() { + await this.vaultTimeoutService.lock(); + } + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + type: "info", + }); + + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + if (confirmed) { + this.messagingService.send("logout", { userId: userId }); + } } ngOnDestroy() { diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.html b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.html rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.html diff --git a/apps/browser/src/popup/settings/await-desktop-dialog.component.ts b/apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts similarity index 100% rename from apps/browser/src/popup/settings/await-desktop-dialog.component.ts rename to apps/browser/src/auth/popup/settings/await-desktop-dialog.component.ts diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.html b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.html similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.html rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.html diff --git a/apps/browser/src/popup/settings/vault-timeout-input.component.ts b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts similarity index 100% rename from apps/browser/src/popup/settings/vault-timeout-input.component.ts rename to apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index bad2e4a9e7..6191d277ad 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -27,9 +30,9 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { this.navigateTo2FA(); } - choose(p: any) { - super.choose(p); - this.twoFactorService.setSelectedProvider(p.type); + override async choose(p: TwoFactorProviderDetails) { + await super.choose(p); + await this.twoFactorService.setSelectedProvider(p.type); this.navigateTo2FA(); } diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index c948f7aa94..bee5da18b5 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -1,3 +1,7 @@ +import { + accountServiceFactory, + AccountServiceInitOptions, +} from "../../../auth/background/service-factories/account-service.factory"; import { UserVerificationServiceInitOptions, userVerificationServiceFactory, @@ -7,6 +11,10 @@ import { eventCollectionServiceFactory, } from "../../../background/service-factories/event-collection-service.factory"; import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; +import { + browserScriptInjectorServiceFactory, + BrowserScriptInjectorServiceInitOptions, +} from "../../../platform/background/service-factories/browser-script-injector-service.factory"; import { CachedServices, factory, @@ -45,7 +53,9 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & UserVerificationServiceInitOptions & - DomainSettingsServiceInitOptions; + DomainSettingsServiceInitOptions & + BrowserScriptInjectorServiceInitOptions & + AccountServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -65,6 +75,8 @@ export function autofillServiceFactory( await domainSettingsServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), + await browserScriptInjectorServiceFactory(cache, opts), + await accountServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 8cdfa0f027..2eb976529f 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -4,40 +4,29 @@ import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { BrowserApi } from "../../platform/browser/browser-api"; - export default class WebRequestBackground { - private pendingAuthRequests: any[] = []; - private webRequest: any; + private pendingAuthRequests: Set = new Set([]); private isFirefox: boolean; constructor( platformUtilsService: PlatformUtilsService, private cipherService: CipherService, private authService: AuthService, + private readonly webRequest: typeof chrome.webRequest, ) { - if (BrowserApi.isManifestVersion(2)) { - this.webRequest = chrome.webRequest; - } this.isFirefox = platformUtilsService.isFirefox(); } - async init() { - if (!this.webRequest || !this.webRequest.onAuthRequired) { - return; - } - + startListening() { this.webRequest.onAuthRequired.addListener( - async (details: any, callback: any) => { - if (!details.url || this.pendingAuthRequests.indexOf(details.requestId) !== -1) { + async (details, callback) => { + if (!details.url || this.pendingAuthRequests.has(details.requestId)) { if (callback) { - callback(); + callback(null); } return; } - - this.pendingAuthRequests.push(details.requestId); - + this.pendingAuthRequests.add(details.requestId); if (this.isFirefox) { // eslint-disable-next-line return new Promise(async (resolve, reject) => { @@ -51,7 +40,7 @@ export default class WebRequestBackground { [this.isFirefox ? "blocking" : "asyncBlocking"], ); - this.webRequest.onCompleted.addListener((details: any) => this.completeAuthRequest(details), { + this.webRequest.onCompleted.addListener((details) => this.completeAuthRequest(details), { urls: ["http://*/*"], }); this.webRequest.onErrorOccurred.addListener( @@ -91,10 +80,7 @@ export default class WebRequestBackground { } } - private completeAuthRequest(details: any) { - const i = this.pendingAuthRequests.indexOf(details.requestId); - if (i > -1) { - this.pendingAuthRequests.splice(i, 1); - } + private completeAuthRequest(details: chrome.webRequest.WebResponseCacheDetails) { + this.pendingAuthRequests.delete(details.requestId); } } diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts index e54f37489b..6ef004f797 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts @@ -11,7 +11,8 @@ import { GENERATE_PASSWORD_ID, NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } 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"; @@ -65,7 +66,7 @@ describe("ContextMenuClickedHandler", () => { let autofill: AutofillAction; let authService: MockProxy; let cipherService: MockProxy; - let stateService: MockProxy; + let accountService: FakeAccountService; let totpService: MockProxy; let eventCollectionService: MockProxy; let userVerificationService: MockProxy; @@ -78,7 +79,7 @@ describe("ContextMenuClickedHandler", () => { autofill = jest.fn, [tab: chrome.tabs.Tab, cipher: CipherView]>(); authService = mock(); cipherService = mock(); - stateService = mock(); + accountService = mockAccountServiceWith("userId" as UserId); totpService = mock(); eventCollectionService = mock(); @@ -88,10 +89,10 @@ describe("ContextMenuClickedHandler", () => { autofill, authService, cipherService, - stateService, totpService, eventCollectionService, userVerificationService, + accountService, ); }); diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 596d6b7235..5ba48a9f27 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,4 +1,7 @@ +import { firstValueFrom, map } from "rxjs"; + import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -17,7 +20,6 @@ import { NOOP_COMMAND_SUFFIX, } from "@bitwarden/common/autofill/constants"; import { EventType } from "@bitwarden/common/enums"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -26,6 +28,7 @@ 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 { accountServiceFactory } from "../../auth/background/service-factories/account-service.factory"; import { authServiceFactory, AuthServiceInitOptions, @@ -37,7 +40,6 @@ import { autofillSettingsServiceFactory } from "../../autofill/background/servic import { eventCollectionServiceFactory } from "../../background/service-factories/event-collection-service.factory"; import { Account } from "../../models/account"; import { CachedServices } from "../../platform/background/service-factories/factory-options"; -import { stateServiceFactory } from "../../platform/background/service-factories/state-service.factory"; import { BrowserApi } from "../../platform/browser/browser-api"; import { passwordGenerationServiceFactory } from "../../tools/background/service_factories/password-generation-service.factory"; import { @@ -71,10 +73,10 @@ export class ContextMenuClickedHandler { private autofillAction: AutofillAction, private authService: AuthService, private cipherService: CipherService, - private stateService: StateService, private totpService: TotpService, private eventCollectionService: EventCollectionService, private userVerificationService: UserVerificationService, + private accountService: AccountService, ) {} static async mv3Create(cachedServices: CachedServices) { @@ -128,10 +130,10 @@ export class ContextMenuClickedHandler { (tab, cipher) => autofillCommand.doAutofillTabWithCipherCommand(tab, cipher), await authServiceFactory(cachedServices, serviceOptions), await cipherServiceFactory(cachedServices, serviceOptions), - await stateServiceFactory(cachedServices, serviceOptions), await totpServiceFactory(cachedServices, serviceOptions), await eventCollectionServiceFactory(cachedServices, serviceOptions), await userVerificationServiceFactory(cachedServices, serviceOptions), + await accountServiceFactory(cachedServices, serviceOptions), ); } @@ -239,9 +241,10 @@ export class ContextMenuClickedHandler { 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.stateService.setLastActive(new Date().getTime()); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + await this.accountService.setAccountActivity(activeUserId, new Date()); switch (info.parentMenuItemId) { case AUTOFILL_ID: case AUTOFILL_IDENTITY_ID: diff --git a/apps/browser/src/autofill/content/autofill-init.spec.ts b/apps/browser/src/autofill/content/autofill-init.spec.ts index b299ddccbf..8f1a8bf992 100644 --- a/apps/browser/src/autofill/content/autofill-init.spec.ts +++ b/apps/browser/src/autofill/content/autofill-init.spec.ts @@ -560,6 +560,17 @@ describe("AutofillInit", () => { }); describe("destroy", () => { + it("clears the timeout used to collect page details on load", () => { + jest.spyOn(window, "clearTimeout"); + + autofillInit.init(); + autofillInit.destroy(); + + expect(window.clearTimeout).toHaveBeenCalledWith( + autofillInit["collectPageDetailsOnLoadTimeout"], + ); + }); + it("removes the extension message listeners", () => { autofillInit.destroy(); diff --git a/apps/browser/src/autofill/content/autofill-init.ts b/apps/browser/src/autofill/content/autofill-init.ts index 2de35dee20..e78a1fb5ee 100644 --- a/apps/browser/src/autofill/content/autofill-init.ts +++ b/apps/browser/src/autofill/content/autofill-init.ts @@ -16,6 +16,7 @@ class AutofillInit implements AutofillInitInterface { private readonly domElementVisibilityService: DomElementVisibilityService; private readonly collectAutofillContentService: CollectAutofillContentService; private readonly insertAutofillContentService: InsertAutofillContentService; + private collectPageDetailsOnLoadTimeout: number | NodeJS.Timeout | undefined; private readonly extensionMessageHandlers: AutofillExtensionMessageHandlers = { collectPageDetails: ({ message }) => this.collectPageDetails(message), collectPageDetailsImmediately: ({ message }) => this.collectPageDetails(message, true), @@ -66,17 +67,19 @@ class AutofillInit implements AutofillInitInterface { * to act on the page. */ private collectPageDetailsOnLoad() { - const sendCollectDetailsMessage = () => - setTimeout( + const sendCollectDetailsMessage = () => { + this.clearCollectPageDetailsOnLoadTimeout(); + this.collectPageDetailsOnLoadTimeout = setTimeout( () => sendExtensionMessage("bgCollectPageDetails", { sender: "autofillInit" }), 250, ); + }; - if (document.readyState === "complete") { + if (globalThis.document.readyState === "complete") { sendCollectDetailsMessage(); } - window.addEventListener("load", sendCollectDetailsMessage); + globalThis.addEventListener("load", sendCollectDetailsMessage); } /** @@ -247,6 +250,15 @@ class AutofillInit implements AutofillInitInterface { this.autofillOverlayContentService.autofillOverlayVisibility = data?.autofillOverlayVisibility; } + /** + * Clears the send collect details message timeout. + */ + private clearCollectPageDetailsOnLoadTimeout() { + if (this.collectPageDetailsOnLoadTimeout) { + clearTimeout(this.collectPageDetailsOnLoadTimeout); + } + } + /** * Sets up the extension message listeners for the content script. */ @@ -288,6 +300,7 @@ class AutofillInit implements AutofillInitInterface { * listeners, timeouts, and object instances to prevent memory leaks. */ destroy() { + this.clearCollectPageDetailsOnLoadTimeout(); chrome.runtime.onMessage.removeListener(this.handleExtensionMessage); this.collectAutofillContentService.destroy(); this.autofillOverlayContentService?.destroy(); diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 4db64f417d..158fde6a56 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -12,6 +12,7 @@ import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abs import { EventType } from "@bitwarden/common/enums"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { @@ -32,6 +33,7 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; import AutofillPageDetails from "../models/autofill-page-details"; @@ -67,13 +69,16 @@ describe("AutofillService", () => { const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); let domainSettingsService: DomainSettingsService; + let scriptInjectorService: BrowserScriptInjectorService; const totpService = mock(); const eventCollectionService = mock(); const logService = mock(); const userVerificationService = mock(); const billingAccountProfileStateService = mock(); + const platformUtilsService = mock(); beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); autofillService = new AutofillService( cipherService, autofillSettingsService, @@ -83,6 +88,8 @@ describe("AutofillService", () => { domainSettingsService, userVerificationService, billingAccountProfileStateService, + scriptInjectorService, + accountService, ); domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); @@ -250,6 +257,7 @@ describe("AutofillService", () => { expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { file: "content/content-message-handler.js", + frameId: 0, ...defaultExecuteScriptOptions, }); }); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 8b33d03419..dd87505441 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; @@ -20,6 +21,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { openVaultItemPasswordRepromptPopout } from "../../vault/popup/utils/vault-popout-window"; import { AutofillPort } from "../enums/autofill-port.enums"; import AutofillField from "../models/autofill-field"; @@ -55,6 +57,8 @@ export default class AutofillService implements AutofillServiceInterface { private domainSettingsService: DomainSettingsService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + private scriptInjectorService: ScriptInjectorService, + private accountService: AccountService, ) {} /** @@ -102,30 +106,44 @@ export default class AutofillService implements AutofillServiceInterface { frameId = 0, triggeringOnPageLoad = true, ): Promise { - const mainAutofillScript = (await this.getOverlayVisibility()) + // Autofill user settings loaded from state can await the active account state indefinitely + // if not guarded by an active account check (e.g. the user is logged in) + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + + let autoFillOnPageLoadIsEnabled = false; + const overlayVisibility = await this.getOverlayVisibility(); + + const mainAutofillScript = overlayVisibility ? "bootstrap-autofill-overlay.js" : "bootstrap-autofill.js"; const injectedScripts = [mainAutofillScript]; - const autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + if (activeAccount) { + autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad(); + } if (triggeringOnPageLoad && autoFillOnPageLoadIsEnabled) { injectedScripts.push("autofiller.js"); - } else { - await BrowserApi.executeScriptInTab(tab.id, { - file: "content/content-message-handler.js", - runAt: "document_start", + } + + if (!triggeringOnPageLoad) { + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { file: "content/content-message-handler.js", runAt: "document_start" }, }); } injectedScripts.push("notificationBar.js", "contextMenuHandler.js"); for (const injectedScript of injectedScripts) { - await BrowserApi.executeScriptInTab(tab.id, { - file: `content/${injectedScript}`, - frameId, - runAt: "document_start", + await this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: `content/${injectedScript}`, + runAt: "document_start", + frame: frameId, + }, }); } } @@ -2064,9 +2082,7 @@ export default class AutofillService implements AutofillServiceInterface { for (let index = 0; index < tabs.length; index++) { const tab = tabs[index]; if (tab.url?.startsWith("http")) { - // 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.injectAutofillScripts(tab, 0, false); + void this.injectAutofillScripts(tab, 0, false); } } } diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index 708489c57e..9c957f6b1b 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -267,6 +267,7 @@ function createPortSpyMock(name: string) { disconnect: jest.fn(), sender: { tab: createChromeTabMock(), + url: "https://jest-testing-website.com", }, }); } diff --git a/apps/browser/src/autofill/spec/fido2-testing-utils.ts b/apps/browser/src/autofill/spec/fido2-testing-utils.ts new file mode 100644 index 0000000000..c9b39c16cc --- /dev/null +++ b/apps/browser/src/autofill/spec/fido2-testing-utils.ts @@ -0,0 +1,74 @@ +import { mock } from "jest-mock-extended"; + +import { + AssertCredentialResult, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +export function createCredentialCreationOptionsMock( + customFields: Partial = {}, +): CredentialCreationOptions { + return mock({ + publicKey: mock({ + authenticatorSelection: { authenticatorAttachment: "platform" }, + excludeCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + pubKeyCredParams: [{ alg: -7, type: "public-key" }], + user: { id: new ArrayBuffer(32), name: "test", displayName: "test" }, + }), + ...customFields, + }); +} + +export function createCreateCredentialResultMock( + customFields: Partial = {}, +): CreateCredentialResult { + return mock({ + credentialId: "mock", + clientDataJSON: "mock", + attestationObject: "mock", + authData: "mock", + publicKey: "mock", + publicKeyAlgorithm: -7, + transports: ["internal"], + ...customFields, + }); +} + +export function createCredentialRequestOptionsMock( + customFields: Partial = {}, +): CredentialRequestOptions { + return mock({ + mediation: "optional", + publicKey: mock({ + allowCredentials: [{ id: new ArrayBuffer(32), type: "public-key" }], + }), + ...customFields, + }); +} + +export function createAssertCredentialResultMock( + customFields: Partial = {}, +): AssertCredentialResult { + return mock({ + credentialId: "mock", + clientDataJSON: "mock", + authenticatorData: "mock", + signature: "mock", + userHandle: "mock", + ...customFields, + }); +} + +export function setupMockedWebAuthnSupport() { + (globalThis as any).PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable = () => Promise.resolve(true); + }; + (globalThis as any).AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {}; + (globalThis as any).AuthenticatorAssertionResponse = + class PolyfillAuthenticatorAssertionResponse {}; + (globalThis as any).navigator.credentials = { + create: jest.fn().mockResolvedValue({}), + get: jest.fn().mockResolvedValue({}), + }; +} diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 69ed4cfa3d..713dfe801c 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1,10 +1,8 @@ -import { firstValueFrom } from "rxjs"; +import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs"; import { PinCryptoServiceAbstraction, PinCryptoService, - LoginStrategyServiceAbstraction, - LoginStrategyService, InternalUserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsService, AuthRequestServiceAbstraction, @@ -29,14 +27,15 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as kdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; @@ -44,14 +43,14 @@ import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/for import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { SsoLoginService } from "@bitwarden/common/auth/services/sso-login.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { @@ -72,6 +71,7 @@ import { } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { DefaultBillingAccountProfileStateService } from "@bitwarden/common/billing/services/account/billing-account-profile-state.service"; +import { ClientType } from "@bitwarden/common/enums"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -82,10 +82,8 @@ import { FileUploadService as FileUploadServiceAbstraction } from "@bitwarden/co import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -95,21 +93,24 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; -import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { ActiveUserStateProvider, @@ -134,7 +135,6 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { NotificationsService } from "@bitwarden/common/services/notifications.service"; import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; -import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/avatar.service"; import { PasswordGenerationService, PasswordGenerationServiceAbstraction, @@ -207,13 +207,18 @@ import { Account } from "../models/account"; import { BrowserApi } from "../platform/browser/browser-api"; import { flagEnabled } from "../platform/flags"; import { UpdateBadge } from "../platform/listeners/update-badge"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ +import { OffscreenDocumentService } from "../platform/offscreen-document/abstractions/offscreen-document"; +import { DefaultOffscreenDocumentService } from "../platform/offscreen-document/offscreen-document.service"; import { BrowserStateService as StateServiceAbstraction } from "../platform/services/abstractions/browser-state.service"; import { BrowserCryptoService } from "../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../platform/services/browser-local-storage.service"; import BrowserMemoryStorageService from "../platform/services/browser-memory-storage.service"; -import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service"; -import BrowserMessagingService from "../platform/services/browser-messaging.service"; +import { BrowserMultithreadEncryptServiceImplementation } from "../platform/services/browser-multithread-encrypt.service.implementation"; +import { BrowserScriptInjectorService } from "../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../platform/services/default-browser-state.service"; import I18nService from "../platform/services/i18n.service"; import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service"; @@ -221,11 +226,14 @@ import { BackgroundPlatformUtilsService } from "../platform/services/platform-ut import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider"; import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service"; +import { BrowserStorageServiceProvider } from "../platform/storage/browser-storage-service.provider"; +import { ForegroundMemoryStorageService } from "../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../platform/utils/from-chrome-runtime-messaging"; import VaultTimeoutService from "../services/vault-timeout/vault-timeout.service"; import FilelessImporterBackground from "../tools/background/fileless-importer.background"; +import { Fido2Background as Fido2BackgroundAbstraction } from "../vault/fido2/background/abstractions/fido2.background"; +import { Fido2Background } from "../vault/fido2/background/fido2.background"; import { BrowserFido2UserInterfaceService } from "../vault/fido2/browser-fido2-user-interface.service"; -import { Fido2Service as Fido2ServiceAbstraction } from "../vault/services/abstractions/fido2.service"; -import Fido2Service from "../vault/services/fido2.service"; import { VaultFilterService } from "../vault/services/vault-filter.service"; import CommandsBackground from "./commands.background"; @@ -234,11 +242,12 @@ import { NativeMessagingBackground } from "./nativeMessaging.background"; import RuntimeBackground from "./runtime.background"; export default class MainBackground { - messagingService: MessagingServiceAbstraction; + messagingService: MessageSender; storageService: BrowserLocalStorageService; secureStorageService: AbstractStorageService; - memoryStorageService: AbstractMemoryStorageService; - memoryStorageForStateProviders: AbstractMemoryStorageService & ObservableStorageService; + memoryStorageService: AbstractStorageService; + memoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; + largeObjectMemoryStorageForStateProviders: AbstractStorageService & ObservableStorageService; i18nService: I18nServiceAbstraction; platformUtilsService: PlatformUtilsServiceAbstraction; logService: LogServiceAbstraction; @@ -264,7 +273,6 @@ export default class MainBackground { containerService: ContainerService; auditService: AuditServiceAbstraction; authService: AuthServiceAbstraction; - loginStrategyService: LoginStrategyServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; @@ -288,7 +296,6 @@ export default class MainBackground { providerService: ProviderServiceAbstraction; keyConnectorService: KeyConnectorServiceAbstraction; userVerificationService: UserVerificationServiceAbstraction; - twoFactorService: TwoFactorServiceAbstraction; vaultFilterService: VaultFilterService; usernameGenerationService: UsernameGenerationServiceAbstraction; encryptService: EncryptService; @@ -307,7 +314,7 @@ export default class MainBackground { configApiService: ConfigApiServiceAbstraction; devicesApiService: DevicesApiServiceAbstraction; devicesService: DevicesServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestServiceAbstraction; accountService: AccountServiceAbstraction; globalStateProvider: GlobalStateProvider; @@ -316,7 +323,7 @@ export default class MainBackground { activeUserStateProvider: ActiveUserStateProvider; derivedStateProvider: DerivedStateProvider; stateProvider: StateProvider; - fido2Service: Fido2ServiceAbstraction; + fido2Background: Fido2BackgroundAbstraction; individualVaultExportService: IndividualVaultExportServiceAbstraction; organizationVaultExportService: OrganizationVaultExportServiceAbstraction; vaultSettingsService: VaultSettingsServiceAbstraction; @@ -324,6 +331,12 @@ export default class MainBackground { stateEventRunnerService: StateEventRunnerService; ssoLoginService: SsoLoginServiceAbstraction; billingAccountProfileStateService: BillingAccountProfileStateService; + // eslint-disable-next-line rxjs/no-exposed-subjects -- Needed to give access to services module + intraprocessMessagingSubject: Subject>; + userAutoUnlockKeyService: UserAutoUnlockKeyService; + scriptInjectorService: BrowserScriptInjectorService; + kdfConfigService: kdfConfigServiceAbstraction; + offscreenDocumentService: OffscreenDocumentService; onUpdatedRan: boolean; onReplacedRan: boolean; @@ -343,10 +356,7 @@ export default class MainBackground { private isSafari: boolean; private nativeMessagingBackground: NativeMessagingBackground; - constructor( - public isPrivateMode: boolean = false, - public popupOnlyContext: boolean = false, - ) { + constructor(public popupOnlyContext: boolean = false) { // Services const lockedCallback = async (userId?: string) => { if (this.notificationsService != null) { @@ -365,37 +375,90 @@ export default class MainBackground { const logoutCallback = async (expired: boolean, userId?: UserId) => await this.logout(expired, userId); - this.messagingService = - this.isPrivateMode && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModeBackgroundService() - : new BrowserMessagingService(); this.logService = new ConsoleLogService(false); this.cryptoFunctionService = new WebCryptoFunctionService(self); this.keyGenerationService = new KeyGenerationService(this.cryptoFunctionService); this.storageService = new BrowserLocalStorageService(); - const mv3MemoryStorageCreator = (partitionName: string) => { - // TODO: Consider using multithreaded encrypt service in popup only context + this.intraprocessMessagingSubject = new Subject>(); + + this.messagingService = MessageSender.combine( + new SubjectMessageSender(this.intraprocessMessagingSubject), + new ChromeMessageSender(this.logService), + ); + + const messageListener = new MessageListener( + merge( + this.intraprocessMessagingSubject.asObservable(), // For messages from the same context + fromChromeRuntimeMessaging(), // For messages from other contexts + ), + ); + + this.offscreenDocumentService = new DefaultOffscreenDocumentService(); + + this.platformUtilsService = new BackgroundPlatformUtilsService( + this.messagingService, + (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), + async () => this.biometricUnlock(), + self, + this.offscreenDocumentService, + ); + + // Creates a session key for mv3 storage of large memory items + const sessionKey = new Lazy(async () => { + // Key already in session storage + const sessionStorage = new BrowserMemoryStorageService(); + const existingKey = await sessionStorage.get("session-key"); + if (existingKey) { + if (sessionStorage.valuesRequireDeserialization) { + return SymmetricCryptoKey.fromJSON(existingKey); + } + return existingKey; + } + + // New key + const { derivedKey } = await this.keyGenerationService.createKeyWithPurpose( + 128, + "ephemeral", + "bitwarden-ephemeral", + ); + await sessionStorage.save("session-key", derivedKey); + return derivedKey; + }); + + const mv3MemoryStorageCreator = () => { + if (this.popupOnlyContext) { + return new ForegroundMemoryStorageService(); + } + return new LocalBackedSessionStorageService( + sessionKey, + this.storageService, new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, false), - this.keyGenerationService, - new BrowserLocalStorageService(), - new BrowserMemoryStorageService(), - partitionName, + this.platformUtilsService, + this.logService, ); }; this.secureStorageService = this.storageService; // secure storage is not supported in browsers, so we use local storage and warn users when it is used - this.memoryStorageService = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateService") - : new MemoryStorageService(); this.memoryStorageForStateProviders = BrowserApi.isManifestVersion(3) - ? mv3MemoryStorageCreator("stateProviders") - : new BackgroundMemoryStorageService(); + ? new BrowserMemoryStorageService() // mv3 stores to storage.session + : popupOnlyContext + ? new ForegroundMemoryStorageService() + : new BackgroundMemoryStorageService(); // mv2 stores to memory + this.memoryStorageService = BrowserApi.isManifestVersion(3) + ? this.memoryStorageForStateProviders // manifest v3 can reuse the same storage. They are split for v2 due to lacking a good sync mechanism, which isn't true for v3 + : popupOnlyContext + ? new ForegroundMemoryStorageService() + : new BackgroundMemoryStorageService(); + this.largeObjectMemoryStorageForStateProviders = BrowserApi.isManifestVersion(3) + ? mv3MemoryStorageCreator() // mv3 stores to local-backed session storage + : this.memoryStorageForStateProviders; // mv2 stores to the same location - const storageServiceProvider = new StorageServiceProvider( + const storageServiceProvider = new BrowserStorageServiceProvider( this.storageService, this.memoryStorageForStateProviders, + this.largeObjectMemoryStorageForStateProviders, ); this.globalStateProvider = new DefaultGlobalStateProvider(storageServiceProvider); @@ -410,14 +473,14 @@ export default class MainBackground { storageServiceProvider, ); - this.encryptService = - flagEnabled("multithreadDecryption") && BrowserApi.isManifestVersion(2) - ? new MultithreadEncryptServiceImplementation( - this.cryptoFunctionService, - this.logService, - true, - ) - : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); + this.encryptService = flagEnabled("multithreadDecryption") + ? new BrowserMultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + this.offscreenDocumentService, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, @@ -432,9 +495,7 @@ export default class MainBackground { this.accountService, this.singleUserStateProvider, ); - this.derivedStateProvider = new BackgroundDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new BackgroundDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, this.singleUserStateProvider, @@ -449,12 +510,6 @@ export default class MainBackground { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider); - this.platformUtilsService = new BackgroundPlatformUtilsService( - this.messagingService, - (clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs), - async () => this.biometricUnlock(), - self, - ); this.tokenService = new TokenService( this.singleUserStateProvider, @@ -470,6 +525,7 @@ export default class MainBackground { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Browser, ); this.stateService = new DefaultBrowserStateService( @@ -489,6 +545,9 @@ export default class MainBackground { this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); + + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new BrowserCryptoService( this.masterPasswordService, this.keyGenerationService, @@ -500,6 +559,7 @@ export default class MainBackground { this.accountService, this.stateProvider, this.biometricStateService, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -554,27 +614,10 @@ export default class MainBackground { this.stateService, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); - - // eslint-disable-next-line - const that = this; - const backgroundMessagingService = new (class extends MessagingServiceAbstraction { - // AuthService should send the messages to the background not popup. - send = (subscriber: string, arg: any = {}) => { - if (BrowserApi.isManifestVersion(3)) { - that.messagingService.send(subscriber, arg); - return; - } - - const message = Object.assign({}, { command: subscriber }, arg); - void that.runtimeBackground.processMessage(message, that as any); - }; - })(); - this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -586,6 +629,7 @@ export default class MainBackground { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); @@ -601,7 +645,7 @@ export default class MainBackground { this.authService = new AuthService( this.accountService, - backgroundMessagingService, + this.messagingService, this.cryptoService, this.apiService, this.stateService, @@ -614,31 +658,6 @@ export default class MainBackground { this.loginEmailService = new LoginEmailService(this.stateProvider); - this.loginStrategyService = new LoginStrategyService( - this.accountService, - this.masterPasswordService, - this.cryptoService, - this.apiService, - this.tokenService, - this.appIdService, - this.platformUtilsService, - backgroundMessagingService, - this.logService, - this.keyConnectorService, - this.environmentService, - this.stateService, - this.twoFactorService, - this.i18nService, - this.encryptService, - this.passwordStrengthService, - this.policyService, - this.deviceTrustCryptoService, - this.authRequestService, - this.userDecryptionOptionsService, - this.globalStateProvider, - this.billingAccountProfileStateService, - ); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -687,6 +706,7 @@ export default class MainBackground { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -701,6 +721,7 @@ export default class MainBackground { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultFilterService = new VaultFilterService( @@ -721,7 +742,6 @@ export default class MainBackground { this.cipherService, this.folderService, this.collectionService, - this.cryptoService, this.platformUtilsService, this.messagingService, this.searchService, @@ -775,6 +795,7 @@ export default class MainBackground { this.avatarService, logoutCallback, this.billingAccountProfileStateService, + this.tokenService, ); this.eventUploadService = new EventUploadService( this.apiService, @@ -791,6 +812,10 @@ export default class MainBackground { ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); + this.scriptInjectorService = new BrowserScriptInjectorService( + this.platformUtilsService, + this.logService, + ); this.autofillService = new AutofillService( this.cipherService, this.autofillSettingsService, @@ -800,6 +825,8 @@ export default class MainBackground { this.domainSettingsService, this.userVerificationService, this.billingAccountProfileStateService, + this.scriptInjectorService, + this.accountService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -819,7 +846,7 @@ export default class MainBackground { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationVaultExportService = new OrganizationVaultExportService( @@ -827,8 +854,8 @@ export default class MainBackground { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( @@ -849,7 +876,6 @@ export default class MainBackground { this.messagingService, ); - this.fido2Service = new Fido2Service(); this.fido2UserInterfaceService = new BrowserFido2UserInterfaceService(this.authService); this.fido2AuthenticatorService = new Fido2AuthenticatorService( this.cipherService, @@ -883,6 +909,7 @@ export default class MainBackground { this.autofillSettingsService, this.vaultTimeoutSettingsService, this.biometricStateService, + this.accountService, ); // Other fields @@ -890,20 +917,26 @@ export default class MainBackground { // Background if (!this.popupOnlyContext) { + this.fido2Background = new Fido2Background( + this.logService, + this.fido2ClientService, + this.vaultSettingsService, + this.scriptInjectorService, + ); this.runtimeBackground = new RuntimeBackground( this, this.autofillService, this.platformUtilsService as BrowserPlatformUtilsService, - this.i18nService, this.notificationsService, - this.stateService, this.autofillSettingsService, this.systemService, this.environmentService, this.messagingService, this.logService, this.configService, - this.fido2Service, + this.fido2Background, + messageListener, + this.accountService, ); this.nativeMessagingBackground = new NativeMessagingBackground( this.accountService, @@ -959,6 +992,7 @@ export default class MainBackground { this.notificationBackground, this.importService, this.syncService, + this.scriptInjectorService, ); this.tabsBackground = new TabsBackground( this, @@ -992,10 +1026,10 @@ export default class MainBackground { }, this.authService, this.cipherService, - this.stateService, this.totpService, this.eventCollectionService, this.userVerificationService, + this.accountService, ); this.contextMenusBackground = new ContextMenusBackground(contextMenuClickedHandler); @@ -1029,61 +1063,62 @@ export default class MainBackground { this.cipherService, ); - if (BrowserApi.isManifestVersion(2)) { + if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { this.webRequestBackground = new WebRequestBackground( this.platformUtilsService, this.cipherService, this.authService, + chrome.webRequest, ); } } + + this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); } async bootstrap() { this.containerService.attachToGlobal(self); - await this.stateService.init({ runMigrations: !this.isPrivateMode }); + // Only the "true" background should run migrations + await this.stateService.init({ runMigrations: !this.popupOnlyContext }); + + // This is here instead of in in the InitService b/c we don't plan for + // side effects to run in the Browser InitService. + const accounts = await firstValueFrom(this.accountService.accounts$); + + const setUserKeyInMemoryPromises = []; + for (const userId of Object.keys(accounts) as UserId[]) { + // For each acct, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access + // the user key from mem. + setUserKeyInMemoryPromises.push( + this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId), + ); + } + await Promise.all(setUserKeyInMemoryPromises); await (this.i18nService as I18nService).init(); (this.eventUploadService as EventUploadService).init(true); - this.twoFactorService.init(); - if (!this.popupOnlyContext) { - await this.vaultTimeoutService.init(true); - await this.runtimeBackground.init(); - await this.notificationBackground.init(); - this.filelessImporterBackground.init(); - await this.commandsBackground.init(); - await this.overlayBackground.init(); - await this.tabsBackground.init(); - this.contextMenusBackground?.init(); - await this.idleBackground.init(); - if (BrowserApi.isManifestVersion(2)) { - await this.webRequestBackground.init(); - } + if (this.popupOnlyContext) { + return; } - if (this.platformUtilsService.isFirefox() && !this.isPrivateMode) { - // Set Private Mode windows to the default icon - they do not share state with the background page - const privateWindows = await BrowserApi.getPrivateModeWindows(); - privateWindows.forEach(async (win) => { - await new UpdateBadge(self).setBadgeIcon("", win.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 - BrowserApi.onWindowCreated(async (win) => { - if (win.incognito) { - await new UpdateBadge(self).setBadgeIcon("", win.id); - } - }); - } + await this.vaultTimeoutService.init(true); + this.fido2Background.init(); + await this.runtimeBackground.init(); + await this.notificationBackground.init(); + this.filelessImporterBackground.init(); + await this.commandsBackground.init(); + await this.overlayBackground.init(); + await this.tabsBackground.init(); + this.contextMenusBackground?.init(); + await this.idleBackground.init(); + this.webRequestBackground?.startListening(); return new Promise((resolve) => { setTimeout(async () => { - if (!this.isPrivateMode) { - await this.refreshBadge(); - } + await this.refreshBadge(); await this.fullSync(true); setTimeout(() => this.notificationsService.init(), 2500); resolve(); @@ -1122,7 +1157,12 @@ export default class MainBackground { */ async switchAccount(userId: UserId) { try { - await this.stateService.setActiveUser(userId); + const currentlyActiveAccount = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + // can be removed once password generation history is migrated to state providers + await this.stateService.clearDecryptedData(currentlyActiveAccount); + await this.accountService.switchAccount(userId); if (userId == null) { this.loginEmailService.setRememberEmail(false); @@ -1159,20 +1199,46 @@ export default class MainBackground { } async logout(expired: boolean, userId?: UserId) { - userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id; + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout({ + first: 2000, + with: () => { + throw new Error("No active account found to logout"); + }, + }), + ), + ); - await this.eventUploadService.uploadEvents(userId as UserId); + const userBeingLoggedOut = userId ?? activeUserId; + + await this.eventUploadService.uploadEvents(userBeingLoggedOut); + + // HACK: We shouldn't wait for the authentication status to change but instead subscribe to the + // authentication status to do various actions. + const logoutPromise = firstValueFrom( + this.authService.authStatusFor$(userBeingLoggedOut).pipe( + filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut), + timeout({ + first: 5_000, + with: () => { + throw new Error("The logout process did not complete in a reasonable amount of time."); + }, + }), + ), + ); await Promise.all([ - this.syncService.setLastSync(new Date(0), userId), - this.cryptoService.clearKeys(userId), - this.cipherService.clear(userId), - this.folderService.clear(userId), - this.collectionService.clear(userId), - this.passwordGenerationService.clear(userId), - this.vaultTimeoutSettingsService.clear(userId), + this.syncService.setLastSync(new Date(0), userBeingLoggedOut), + this.cryptoService.clearKeys(userBeingLoggedOut), + this.cipherService.clear(userBeingLoggedOut), + this.folderService.clear(userBeingLoggedOut), + this.collectionService.clear(userBeingLoggedOut), + this.passwordGenerationService.clear(userBeingLoggedOut), + this.vaultTimeoutSettingsService.clear(userBeingLoggedOut), this.vaultFilterService.clear(), - this.biometricStateService.logout(userId), + this.biometricStateService.logout(userBeingLoggedOut), /* We intentionally do not clear: * - autofillSettingsService * - badgeSettingsService @@ -1183,16 +1249,28 @@ export default class MainBackground { //Needs to be checked before state is cleaned const needStorageReseed = await this.needsStorageReseed(); - const newActiveUser = await this.stateService.clean({ userId: userId }); + const newActiveUser = + userBeingLoggedOut === activeUserId + ? await firstValueFrom(this.accountService.nextUpAccount$.pipe(map((a) => a?.id))) + : null; - await this.stateEventRunnerService.handleEvent("logout", userId); + await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.accountService.clean(userBeingLoggedOut); + await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); + + // HACK: Wait for the user logging outs authentication status to transition to LoggedOut + await logoutPromise; + + await this.switchAccount(newActiveUser); if (newActiveUser != null) { // we have a new active user, do not continue tearing down application - await this.switchAccount(newActiveUser as UserId); this.messagingService.send("switchAccountFinish"); } else { - this.messagingService.send("doneLoggingOut", { expired: expired, userId: userId }); + this.messagingService.send("doneLoggingOut", { + expired: expired, + userId: userBeingLoggedOut, + }); } if (needStorageReseed) { diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts index faf2e6e2cc..5ac9961147 100644 --- a/apps/browser/src/background/nativeMessaging.background.ts +++ b/apps/browser/src/background/nativeMessaging.background.ts @@ -204,6 +204,8 @@ export class NativeMessagingBackground { this.privateKey = null; this.connected = false; + this.logService.error("NativeMessaging port disconnected because of error: " + error); + const reason = error != null ? "desktopIntegrationDisabled" : null; reject(new Error(reason)); }); @@ -397,7 +399,7 @@ export class NativeMessagingBackground { // 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" }, null); + this.runtimeBackground.processMessage({ command: "unlocked" }); } break; } diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts index a88bc051d8..1db32659d2 100644 --- a/apps/browser/src/background/runtime.background.ts +++ b/apps/browser/src/background/runtime.background.ts @@ -1,16 +1,18 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, mergeMap } from "rxjs"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { SystemService } from "@bitwarden/common/platform/abstractions/system.service"; +import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { MessageListener, isExternalMessage } from "../../../../libs/common/src/platform/messaging"; import { closeUnlockPopout, openSsoAuthResultPopout, @@ -19,11 +21,9 @@ import { import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; import { AutofillService } from "../autofill/services/abstractions/autofill.service"; import { BrowserApi } from "../platform/browser/browser-api"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserEnvironmentService } from "../platform/services/browser-environment.service"; import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service"; -import { AbortManager } from "../vault/background/abort-manager"; -import { Fido2Service } from "../vault/services/abstractions/fido2.service"; +import { Fido2Background } from "../vault/fido2/background/abstractions/fido2.background"; import MainBackground from "./main.background"; @@ -32,22 +32,21 @@ export default class RuntimeBackground { private pageDetailsToAutoFill: any[] = []; private onInstalledReason: string = null; private lockedVaultPendingNotifications: LockedVaultPendingNotificationsData[] = []; - private abortManager = new AbortManager(); constructor( private main: MainBackground, private autofillService: AutofillService, private platformUtilsService: BrowserPlatformUtilsService, - private i18nService: I18nService, private notificationsService: NotificationsService, - private stateService: BrowserStateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private systemService: SystemService, private environmentService: BrowserEnvironmentService, private messagingService: MessagingService, private logService: LogService, private configService: ConfigService, - private fido2Service: Fido2Service, + private fido2Background: Fido2Background, + private messageListener: MessageListener, + private accountService: AccountService, ) { // onInstalled listener must be wired up before anything else, so we do it in the ctor chrome.runtime.onInstalled.addListener((details: any) => { @@ -64,107 +63,60 @@ export default class RuntimeBackground { const backgroundMessageListener = ( msg: any, sender: chrome.runtime.MessageSender, - sendResponse: any, + sendResponse: (response: any) => void, ) => { - const messagesWithResponse = [ - "checkFido2FeatureEnabled", - "fido2RegisterCredentialRequest", - "fido2GetCredentialRequest", - "biometricUnlock", - ]; + const messagesWithResponse = ["biometricUnlock"]; if (messagesWithResponse.includes(msg.command)) { - this.processMessage(msg, sender).then( + this.processMessageWithSender(msg, sender).then( (value) => sendResponse({ result: value }), (error) => sendResponse({ error: { ...error, message: error.message } }), ); return 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 - this.processMessage(msg, sender); + void this.processMessageWithSender(msg, sender).catch((err) => + this.logService.error( + `Error while processing message in RuntimeBackground '${msg?.command}'.`, + err, + ), + ); return false; }; + this.messageListener.allMessages$ + .pipe( + mergeMap(async (message: any) => { + try { + await this.processMessage(message); + } catch (err) { + this.logService.error(err); + } + }), + ) + .subscribe(); + + // For messages that require the full on message interface BrowserApi.messageListener("runtime.background", backgroundMessageListener); - if (this.main.popupOnlyContext) { - (self as any).bitwardenBackgroundMessageListener = backgroundMessageListener; - } } - async processMessage(msg: any, sender: chrome.runtime.MessageSender) { + // Messages that need the chrome sender and send back a response need to be registered in this method. + async processMessageWithSender(msg: any, sender: chrome.runtime.MessageSender) { switch (msg.command) { - case "loggedIn": - case "unlocked": { - let item: LockedVaultPendingNotificationsData; - - if (msg.command === "loggedIn") { - await this.sendBwInstalledMessageToVault(); - } - - if (this.lockedVaultPendingNotifications?.length > 0) { - item = this.lockedVaultPendingNotifications.pop(); - await closeUnlockPopout(); - } - - await this.notificationsService.updateConnection(msg.command === "loggedIn"); - await this.main.refreshBadge(); - await this.main.refreshMenu(false); - this.systemService.cancelProcessReload(); - - if (item) { - await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); - await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); - await BrowserApi.tabSendMessageData( - item.commandToRetry.sender.tab, - "unlockCompleted", - item, - ); - } - break; - } - case "addToLockedVaultPendingNotifications": - this.lockedVaultPendingNotifications.push(msg.data); - break; - case "logout": - await this.main.logout(msg.expired, msg.userId); - break; - case "syncCompleted": - if (msg.successfully) { - setTimeout(async () => { - await this.main.refreshBadge(); - await this.main.refreshMenu(); - }, 2000); - await this.configService.ensureConfigFetched(); - } - break; - case "openPopup": - await this.main.openPopup(); - break; case "triggerAutofillScriptInjection": await this.autofillService.injectAutofillScripts(sender.tab, sender.frameId); break; case "bgCollectPageDetails": await this.main.collectPageDetailsForContentScript(sender.tab, msg.sender, sender.frameId); break; - case "bgUpdateContextMenu": - case "editedCipher": - case "addedCipher": - case "deletedCipher": - await this.main.refreshBadge(); - await this.main.refreshMenu(); - break; - case "bgReseedStorage": - await this.main.reseedStorage(); - break; case "collectPageDetailsResponse": switch (msg.sender) { case "autofiller": case "autofill_cmd": { - // 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.stateService.setLastActive(new Date().getTime()); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + await this.accountService.setAccountActivity(activeUserId, new Date()); const totpCode = await this.autofillService.doAutoFillActiveTab( [ { @@ -221,6 +173,72 @@ export default class RuntimeBackground { break; } break; + case "biometricUnlock": { + const result = await this.main.biometricUnlock(); + return result; + } + } + } + + async processMessage(msg: any) { + switch (msg.command) { + case "loggedIn": + case "unlocked": { + let item: LockedVaultPendingNotificationsData; + + if (msg.command === "loggedIn") { + await this.sendBwInstalledMessageToVault(); + } + + if (this.lockedVaultPendingNotifications?.length > 0) { + item = this.lockedVaultPendingNotifications.pop(); + await closeUnlockPopout(); + } + + await this.notificationsService.updateConnection(msg.command === "loggedIn"); + await this.main.refreshBadge(); + await this.main.refreshMenu(false); + this.systemService.cancelProcessReload(); + + if (item) { + await BrowserApi.focusWindow(item.commandToRetry.sender.tab.windowId); + await BrowserApi.focusTab(item.commandToRetry.sender.tab.id); + await BrowserApi.tabSendMessageData( + item.commandToRetry.sender.tab, + "unlockCompleted", + item, + ); + } + break; + } + case "addToLockedVaultPendingNotifications": + this.lockedVaultPendingNotifications.push(msg.data); + break; + case "logout": + await this.main.logout(msg.expired, msg.userId); + break; + case "syncCompleted": + if (msg.successfully) { + setTimeout(async () => { + await this.main.refreshBadge(); + await this.main.refreshMenu(); + }, 2000); + await this.configService.ensureConfigFetched(); + } + break; + case "openPopup": + await this.main.openPopup(); + break; + case "bgUpdateContextMenu": + case "editedCipher": + case "addedCipher": + case "deletedCipher": + await this.main.refreshBadge(); + await this.main.refreshMenu(); + break; + case "bgReseedStorage": + await this.main.reseedStorage(); + break; case "authResult": { const env = await firstValueFrom(this.environmentService.environment$); const vaultUrl = env.getWebVaultUrl(); @@ -255,7 +273,9 @@ export default class RuntimeBackground { break; } case "reloadPopup": - this.messagingService.send("reloadPopup"); + if (isExternalMessage(msg)) { + this.messagingService.send("reloadPopup"); + } break; case "emailVerificationRequired": this.messagingService.send("showDialog", { @@ -269,46 +289,6 @@ export default class RuntimeBackground { case "getClickedElementResponse": this.platformUtilsService.copyToClipboard(msg.identifier); break; - case "triggerFido2ContentScriptInjection": - await this.fido2Service.injectFido2ContentScripts(sender); - break; - case "fido2AbortRequest": - this.abortManager.abort(msg.abortedRequestId); - break; - case "checkFido2FeatureEnabled": - return await this.main.fido2ClientService.isFido2FeatureEnabled(msg.hostname, msg.origin); - case "fido2RegisterCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.createCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); - case "fido2GetCredentialRequest": - return await this.abortManager.runWithAbortController( - msg.requestId, - async (abortController) => { - try { - return await this.main.fido2ClientService.assertCredential( - msg.data, - sender.tab, - abortController, - ); - } finally { - await BrowserApi.focusTab(sender.tab.id); - await BrowserApi.focusWindow(sender.tab.windowId); - } - }, - ); case "switchAccount": { await this.main.switchAccount(msg.userId); break; @@ -317,9 +297,6 @@ export default class RuntimeBackground { await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs); break; } - case "biometricUnlock": { - return await this.main.biometricUnlock(); - } } } @@ -343,15 +320,15 @@ export default class RuntimeBackground { private async checkOnInstalled() { setTimeout(async () => { - // 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.autofillService.loadAutofillScriptsOnInstall(); + void this.fido2Background.injectFido2ContentScriptsInAllTabs(); + void this.autofillService.loadAutofillScriptsOnInstall(); if (this.onInstalledReason != null) { if (this.onInstalledReason === "install") { - // 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/browser-start/"); + if (!devFlagEnabled("skipWelcomeOnInstall")) { + void BrowserApi.createNewTab("https://bitwarden.com/browser-start/"); + } + await this.autofillSettingsService.setInlineMenuVisibility( AutofillOverlayVisibility.OnFieldFocus, ); diff --git a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts index 14f055114b..0b176c28f1 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-service.factory.ts @@ -12,10 +12,6 @@ import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, } from "../../auth/background/service-factories/master-password-service.factory"; -import { - CryptoServiceInitOptions, - cryptoServiceFactory, -} from "../../platform/background/service-factories/crypto-service.factory"; import { CachedServices, factory, @@ -70,7 +66,6 @@ export type VaultTimeoutServiceInitOptions = VaultTimeoutServiceFactoryOptions & CipherServiceInitOptions & FolderServiceInitOptions & CollectionServiceInitOptions & - CryptoServiceInitOptions & PlatformUtilsServiceInitOptions & MessagingServiceInitOptions & SearchServiceInitOptions & @@ -94,7 +89,6 @@ export function vaultTimeoutServiceFactory( await cipherServiceFactory(cache, opts), await folderServiceFactory(cache, opts), await collectionServiceFactory(cache, opts), - await cryptoServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), await messagingServiceFactory(cache, opts), await searchServiceFactory(cache, opts), diff --git a/apps/browser/src/popup/settings/premium.component.html b/apps/browser/src/billing/popup/settings/premium.component.html similarity index 100% rename from apps/browser/src/popup/settings/premium.component.html rename to apps/browser/src/billing/popup/settings/premium.component.html diff --git a/apps/browser/src/popup/settings/premium.component.ts b/apps/browser/src/billing/popup/settings/premium.component.ts similarity index 100% rename from apps/browser/src/popup/settings/premium.component.ts rename to apps/browser/src/billing/popup/settings/premium.component.ts diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index aec7523d5e..fc0ff51230 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": "2024.4.1", + "version": "2024.5.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -22,13 +22,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], @@ -67,7 +60,8 @@ "clipboardWrite", "idle", "webRequest", - "webRequestBlocking" + "webRequestBlocking", + "webNavigation" ], "optional_permissions": ["nativeMessaging", "privacy"], "content_security_policy": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index d67b4affab..0720b65a91 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": "2024.4.1", + "version": "2024.5.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", @@ -23,13 +23,6 @@ "exclude_matches": ["*://*/*.xml*", "file:///*.xml*"], "run_at": "document_start" }, - { - "all_frames": true, - "js": ["content/fido2/trigger-fido2-content-script-injection.js"], - "matches": ["https://*/*"], - "exclude_matches": ["https://*/*.xml*"], - "run_at": "document_start" - }, { "all_frames": true, "css": ["content/autofill.css"], @@ -58,7 +51,7 @@ "default_popup": "popup/index.html" }, "permissions": [ - "", + "activeTab", "tabs", "contextMenus", "storage", @@ -66,12 +59,13 @@ "clipboardRead", "clipboardWrite", "idle", - "alarms", "scripting", - "offscreen" + "offscreen", + "webRequest", + "webRequestAuthProvider" ], "optional_permissions": ["nativeMessaging", "privacy"], - "host_permissions": ["*://*/*"], + "host_permissions": ["https://*/*", "http://*/*"], "content_security_policy": { "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'", "sandbox": "sandbox allow-scripts; script-src 'self'" diff --git a/apps/browser/src/platform/background.ts b/apps/browser/src/platform/background.ts index 9c3510178c..a48c420e77 100644 --- a/apps/browser/src/platform/background.ts +++ b/apps/browser/src/platform/background.ts @@ -5,16 +5,11 @@ import MainBackground from "../background/main.background"; import { BrowserApi } from "./browser/browser-api"; const logService = new ConsoleLogService(false); +if (BrowserApi.isManifestVersion(3)) { + startHeartbeat().catch((error) => logService.error(error)); +} const bitwardenMain = ((self as any).bitwardenMain = new MainBackground()); -bitwardenMain - .bootstrap() - .then(() => { - // Finished bootstrapping - if (BrowserApi.isManifestVersion(3)) { - startHeartbeat().catch((error) => logService.error(error)); - } - }) - .catch((error) => logService.error(error)); +bitwardenMain.bootstrap().catch((error) => logService.error(error)); /** * Tracks when a service worker was last alive and extends the service worker diff --git a/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts new file mode 100644 index 0000000000..e9a8ee379a --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/browser-script-injector-service.factory.ts @@ -0,0 +1,33 @@ +import { + LogServiceInitOptions, + logServiceFactory, +} from "../../background/service-factories/log-service.factory"; +import { BrowserScriptInjectorService } from "../../services/browser-script-injector.service"; + +import { CachedServices, FactoryOptions, factory } from "./factory-options"; +import { + PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, +} from "./platform-utils-service.factory"; + +type BrowserScriptInjectorServiceOptions = FactoryOptions; + +export type BrowserScriptInjectorServiceInitOptions = BrowserScriptInjectorServiceOptions & + PlatformUtilsServiceInitOptions & + LogServiceInitOptions; + +export function browserScriptInjectorServiceFactory( + cache: { browserScriptInjectorService?: BrowserScriptInjectorService } & CachedServices, + opts: BrowserScriptInjectorServiceInitOptions, +): Promise { + return factory( + cache, + "browserScriptInjectorService", + opts, + async () => + new BrowserScriptInjectorService( + await platformUtilsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), + ), + ); +} diff --git a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts index ed4fde162c..1f848e1d0f 100644 --- a/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/crypto-service.factory.ts @@ -4,6 +4,10 @@ import { AccountServiceInitOptions, accountServiceFactory, } from "../../../auth/background/service-factories/account-service.factory"; +import { + KdfConfigServiceInitOptions, + kdfConfigServiceFactory, +} from "../../../auth/background/service-factories/kdf-config-service.factory"; import { internalMasterPasswordServiceFactory, MasterPasswordServiceInitOptions, @@ -18,7 +22,10 @@ import { } from "../../background/service-factories/log-service.factory"; import { BrowserCryptoService } from "../../services/browser-crypto.service"; -import { biometricStateServiceFactory } from "./biometric-state-service.factory"; +import { + BiometricStateServiceInitOptions, + biometricStateServiceFactory, +} from "./biometric-state-service.factory"; import { cryptoFunctionServiceFactory, CryptoFunctionServiceInitOptions, @@ -46,7 +53,9 @@ export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & LogServiceInitOptions & StateServiceInitOptions & AccountServiceInitOptions & - StateProviderInitOptions; + StateProviderInitOptions & + BiometricStateServiceInitOptions & + KdfConfigServiceInitOptions; export function cryptoServiceFactory( cache: { cryptoService?: AbstractCryptoService } & CachedServices, @@ -68,6 +77,7 @@ export function cryptoServiceFactory( await accountServiceFactory(cache, opts), await stateProviderFactory(cache, opts), await biometricStateServiceFactory(cache, opts), + await kdfConfigServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts index 4f329c93d5..3c3900144b 100644 --- a/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts +++ b/apps/browser/src/platform/background/service-factories/derived-state-provider.factory.ts @@ -3,15 +3,10 @@ import { DerivedStateProvider } from "@bitwarden/common/platform/state"; import { BackgroundDerivedStateProvider } from "../../state/background-derived-state.provider"; import { CachedServices, FactoryOptions, factory } from "./factory-options"; -import { - MemoryStorageServiceInitOptions, - observableMemoryStorageServiceFactory, -} from "./storage-service.factory"; type DerivedStateProviderFactoryOptions = FactoryOptions; -export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions & - MemoryStorageServiceInitOptions; +export type DerivedStateProviderInitOptions = DerivedStateProviderFactoryOptions; export async function derivedStateProviderFactory( cache: { derivedStateProvider?: DerivedStateProvider } & CachedServices, @@ -21,7 +16,6 @@ export async function derivedStateProviderFactory( cache, "derivedStateProvider", opts, - async () => - new BackgroundDerivedStateProvider(await observableMemoryStorageServiceFactory(cache, opts)), + async () => new BackgroundDerivedStateProvider(), ); } diff --git a/apps/browser/src/platform/background/service-factories/message-sender.factory.ts b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts new file mode 100644 index 0000000000..6f50b4b8f5 --- /dev/null +++ b/apps/browser/src/platform/background/service-factories/message-sender.factory.ts @@ -0,0 +1,17 @@ +import { MessageSender } from "@bitwarden/common/platform/messaging"; + +import { CachedServices, factory, FactoryOptions } from "./factory-options"; + +type MessagingServiceFactoryOptions = FactoryOptions; + +export type MessageSenderInitOptions = MessagingServiceFactoryOptions; + +export function messageSenderFactory( + cache: { messagingService?: MessageSender } & CachedServices, + opts: MessageSenderInitOptions, +): Promise { + // NOTE: Name needs to match that of MainBackground property until we delete these. + return factory(cache, "messagingService", opts, () => { + throw new Error("Not implemented, not expected to be used."); + }); +} diff --git a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts index 46852712aa..20c6e3f424 100644 --- a/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/messaging-service.factory.ts @@ -1,19 +1,5 @@ -import { MessagingService as AbstractMessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { - CachedServices, - factory, - FactoryOptions, -} from "../../background/service-factories/factory-options"; -import BrowserMessagingService from "../../services/browser-messaging.service"; - -type MessagingServiceFactoryOptions = FactoryOptions; - -export type MessagingServiceInitOptions = MessagingServiceFactoryOptions; - -export function messagingServiceFactory( - cache: { messagingService?: AbstractMessagingService } & CachedServices, - opts: MessagingServiceInitOptions, -): Promise { - return factory(cache, "messagingService", opts, () => new BrowserMessagingService()); -} +// Export old messaging service stuff to minimize changes +export { + messageSenderFactory as messagingServiceFactory, + MessageSenderInitOptions as MessagingServiceInitOptions, +} from "./message-sender.factory"; diff --git a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts index a49699a615..090531f7cf 100644 --- a/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts +++ b/apps/browser/src/platform/background/service-factories/migration-runner.factory.ts @@ -1,3 +1,4 @@ +import { ClientType } from "@bitwarden/common/enums"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -27,6 +28,7 @@ export async function migrationRunnerFactory( await diskStorageServiceFactory(cache, opts), await logServiceFactory(cache, opts), new MigrationBuilderService(), + ClientType.Browser, ), ); } diff --git a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts index 6f46d87418..2cd34ba412 100644 --- a/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/platform-utils-service.factory.ts @@ -30,6 +30,7 @@ export function platformUtilsServiceFactory( opts.platformUtilsServiceOptions.clipboardWriteCallback, opts.platformUtilsServiceOptions.biometricCallback, opts.platformUtilsServiceOptions.win, + null, ), ); } diff --git a/apps/browser/src/platform/background/service-factories/state-service.factory.ts b/apps/browser/src/platform/background/service-factories/state-service.factory.ts index 5567e00990..026a29668e 100644 --- a/apps/browser/src/platform/background/service-factories/state-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/state-service.factory.ts @@ -30,7 +30,6 @@ import { type StateServiceFactoryOptions = FactoryOptions & { stateServiceOptions: { - useAccountCache?: boolean; stateFactory: StateFactory; }; }; @@ -64,7 +63,6 @@ export async function stateServiceFactory( await environmentServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await migrationRunnerFactory(cache, opts), - opts.stateServiceOptions.useAccountCache, ), ); // TODO: If we run migration through a chrome installed/updated event we can turn off running migrations diff --git a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts index 19d5a9c140..764842d751 100644 --- a/apps/browser/src/platform/background/service-factories/storage-service.factory.ts +++ b/apps/browser/src/platform/background/service-factories/storage-service.factory.ts @@ -1,8 +1,9 @@ import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { BrowserApi } from "../../browser/browser-api"; @@ -17,6 +18,11 @@ import { KeyGenerationServiceInitOptions, keyGenerationServiceFactory, } from "./key-generation-service.factory"; +import { LogServiceInitOptions, logServiceFactory } from "./log-service.factory"; +import { + PlatformUtilsServiceInitOptions, + platformUtilsServiceFactory, +} from "./platform-utils-service.factory"; export type DiskStorageServiceInitOptions = FactoryOptions; export type SecureStorageServiceInitOptions = FactoryOptions; @@ -25,7 +31,9 @@ export type MemoryStorageServiceInitOptions = FactoryOptions & EncryptServiceInitOptions & KeyGenerationServiceInitOptions & DiskStorageServiceInitOptions & - SessionStorageServiceInitOptions; + SessionStorageServiceInitOptions & + LogServiceInitOptions & + PlatformUtilsServiceInitOptions; export function diskStorageServiceFactory( cache: { diskStorageService?: AbstractStorageService } & CachedServices, @@ -57,17 +65,29 @@ export function sessionStorageServiceFactory( } export function memoryStorageServiceFactory( - cache: { memoryStorageService?: AbstractMemoryStorageService } & CachedServices, + cache: { memoryStorageService?: AbstractStorageService } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { if (BrowserApi.isManifestVersion(3)) { return new LocalBackedSessionStorageService( - await encryptServiceFactory(cache, opts), - await keyGenerationServiceFactory(cache, opts), + new Lazy(async () => { + const existingKey = await ( + await sessionStorageServiceFactory(cache, opts) + ).get("session-key"); + if (existingKey) { + return existingKey; + } + const { derivedKey } = await ( + await keyGenerationServiceFactory(cache, opts) + ).createKeyWithPurpose(128, "ephemeral", "bitwarden-ephemeral"); + await (await sessionStorageServiceFactory(cache, opts)).save("session-key", derivedKey); + return derivedKey; + }), await diskStorageServiceFactory(cache, opts), - await sessionStorageServiceFactory(cache, opts), - "serviceFactories", + await encryptServiceFactory(cache, opts), + await platformUtilsServiceFactory(cache, opts), + await logServiceFactory(cache, opts), ); } return new MemoryStorageService(); @@ -76,10 +96,10 @@ export function memoryStorageServiceFactory( export function observableMemoryStorageServiceFactory( cache: { - memoryStorageService?: AbstractMemoryStorageService & ObservableStorageService; + memoryStorageService?: AbstractStorageService & ObservableStorageService; } & CachedServices, opts: MemoryStorageServiceInitOptions, -): Promise { +): Promise { return factory(cache, "memoryStorageService", opts, async () => { return new BackgroundMemoryStorageService(); }); diff --git a/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts new file mode 100644 index 0000000000..8a20f3e999 --- /dev/null +++ b/apps/browser/src/platform/browser/browser-api.register-content-scripts-polyfill.ts @@ -0,0 +1,435 @@ +/** + * MIT License + * + * Copyright (c) Federico Brigante (https://fregante.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * + * @see https://github.com/fregante/content-scripts-register-polyfill + * @version 4.0.2 + */ +import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; + +import { BrowserApi } from "./browser-api"; + +let registerContentScripts: ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) => Promise; +export async function registerContentScriptsPolyfill( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback?: (registeredContentScript: browser.contentScripts.RegisteredContentScript) => void, +) { + if (!registerContentScripts) { + registerContentScripts = buildRegisterContentScriptsPolyfill(); + } + + return registerContentScripts(contentScriptOptions, callback); +} + +function buildRegisterContentScriptsPolyfill() { + const logService = new ConsoleLogService(false); + const chromeProxy = globalThis.chrome && NestedProxy(globalThis.chrome); + const patternValidationRegex = + /^(https?|wss?|file|ftp|\*):\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^file:\/\/\/.*$|^resource:\/\/(\*|\*\.[^*/]+|[^*/]+)\/.*$|^about:/; + const isFirefox = globalThis.navigator?.userAgent.includes("Firefox/"); + const gotScripting = Boolean(globalThis.chrome?.scripting); + const gotNavigation = typeof chrome === "object" && "webNavigation" in chrome; + + function NestedProxy(target: T): T { + return new Proxy(target, { + get(target, prop) { + if (!target[prop as keyof T]) { + return; + } + + if (typeof target[prop as keyof T] !== "function") { + return NestedProxy(target[prop as keyof T]); + } + + return (...arguments_: any[]) => + new Promise((resolve, reject) => { + target[prop as keyof T](...arguments_, (result: any) => { + if (chrome.runtime.lastError) { + reject(new Error(chrome.runtime.lastError.message)); + } else { + resolve(result); + } + }); + }); + }, + }); + } + + function assertValidPattern(matchPattern: string) { + if (!isValidPattern(matchPattern)) { + throw new Error( + `${matchPattern} is an invalid pattern, it must match ${String(patternValidationRegex)}`, + ); + } + } + + function isValidPattern(matchPattern: string) { + return matchPattern === "" || patternValidationRegex.test(matchPattern); + } + + function getRawPatternRegex(matchPattern: string) { + assertValidPattern(matchPattern); + let [, protocol, host = "", pathname] = matchPattern.split(/(^[^:]+:[/][/])([^/]+)?/); + protocol = protocol + .replace("*", isFirefox ? "(https?|wss?)" : "https?") + .replaceAll(/[/]/g, "[/]"); + + if (host === "*") { + host = "[^/]+"; + } else if (host) { + host = host + .replace(/^[*][.]/, "([^/]+.)*") + .replaceAll(/[.]/g, "[.]") + .replace(/[*]$/, "[^.]+"); + } + + pathname = pathname + .replaceAll(/[/]/g, "[/]") + .replaceAll(/[.]/g, "[.]") + .replaceAll(/[*]/g, ".*"); + + return "^" + protocol + host + "(" + pathname + ")?$"; + } + + function patternToRegex(...matchPatterns: string[]) { + if (matchPatterns.length === 0) { + return /$./; + } + + if (matchPatterns.includes("")) { + // regex + return /^(https?|file|ftp):[/]+/; + } + + if (matchPatterns.includes("*://*/*")) { + // all stars regex + return isFirefox ? /^(https?|wss?):[/][/][^/]+([/].*)?$/ : /^https?:[/][/][^/]+([/].*)?$/; + } + + return new RegExp(matchPatterns.map((x) => getRawPatternRegex(x)).join("|")); + } + + function castAllFramesTarget(target: number | { tabId: number; frameId: number }) { + if (typeof target === "object") { + return { ...target, allFrames: false }; + } + + return { + tabId: target, + frameId: undefined, + allFrames: true, + }; + } + + function castArray(possibleArray: any | any[]) { + if (Array.isArray(possibleArray)) { + return possibleArray; + } + + return [possibleArray]; + } + + function arrayOrUndefined(value?: number) { + return value === undefined ? undefined : [value]; + } + + async function insertCSS( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const everyInsertion = Promise.all( + files.map(async (content) => { + if (typeof content === "string") { + content = { file: content }; + } + + if (gotScripting) { + return chrome.scripting.insertCSS({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: "file" in content ? [content.file] : undefined, + css: "code" in content ? content.code : undefined, + }); + } + + return chromeProxy.tabs.insertCSS(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt: runAt ?? "document_start", + }); + }), + ); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(everyInsertion); + } else { + await everyInsertion; + } + } + function assertNoCode(files: browser.extensionTypes.ExtensionFileOrCode[]) { + if (files.some((content) => "code" in content)) { + throw new Error("chrome.scripting does not support injecting strings of `code`"); + } + } + + async function executeScript( + { + tabId, + frameId, + files, + allFrames, + matchAboutBlank, + runAt, + }: { + tabId: number; + frameId?: number; + files: browser.extensionTypes.ExtensionFileOrCode[]; + allFrames: boolean; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + { ignoreTargetErrors }: { ignoreTargetErrors?: boolean } = {}, + ) { + const normalizedFiles = files.map((file) => (typeof file === "string" ? { file } : file)); + + if (gotScripting) { + assertNoCode(normalizedFiles); + const injection = chrome.scripting.executeScript({ + target: { + tabId, + frameIds: arrayOrUndefined(frameId), + allFrames: frameId === undefined ? allFrames : undefined, + }, + files: normalizedFiles.map(({ file }: { file: string }) => file), + }); + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(injection); + } else { + await injection; + } + + return; + } + + const executions = []; + for (const content of normalizedFiles) { + if ("code" in content) { + await executions.at(-1); + } + + executions.push( + chromeProxy.tabs.executeScript(tabId, { + ...content, + matchAboutBlank, + allFrames, + frameId, + runAt, + }), + ); + } + + if (ignoreTargetErrors) { + await catchTargetInjectionErrors(Promise.all(executions)); + } else { + await Promise.all(executions); + } + } + + async function injectContentScript( + where: { tabId: number; frameId: number }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const targets = castArray(where); + await Promise.all( + targets.map(async (target) => + injectContentScriptInSpecificTarget(castAllFramesTarget(target), scripts, options), + ), + ); + } + + async function injectContentScriptInSpecificTarget( + { frameId, tabId, allFrames }: { frameId?: number; tabId: number; allFrames: boolean }, + scripts: { + css: browser.extensionTypes.ExtensionFileOrCode[]; + js: browser.extensionTypes.ExtensionFileOrCode[]; + matchAboutBlank: boolean; + runAt: browser.extensionTypes.RunAt; + }, + options = {}, + ) { + const injections = castArray(scripts).flatMap((script) => [ + insertCSS( + { + tabId, + frameId, + allFrames, + files: script.css ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + executeScript( + { + tabId, + frameId, + allFrames, + files: script.js ?? [], + matchAboutBlank: script.matchAboutBlank ?? script.match_about_blank, + runAt: script.runAt ?? script.run_at, + }, + options, + ), + ]); + await Promise.all(injections); + } + + async function catchTargetInjectionErrors(promise: Promise) { + try { + await promise; + } catch (error) { + const targetErrors = + /^No frame with id \d+ in tab \d+.$|^No tab with id: \d+.$|^The tab was closed.$|^The frame was removed.$/; + if (!targetErrors.test(error?.message)) { + throw error; + } + } + } + + async function isOriginPermitted(url: string) { + return chromeProxy.permissions.contains({ + origins: [new URL(url).origin + "/*"], + }); + } + + return async ( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + callback: CallableFunction, + ) => { + const { + js = [], + css = [], + matchAboutBlank, + matches = [], + excludeMatches, + runAt, + } = contentScriptOptions; + let { allFrames } = contentScriptOptions; + + if (gotNavigation) { + allFrames = false; + } else if (allFrames) { + logService.warning( + "`allFrames: true` requires the `webNavigation` permission to work correctly: https://github.com/fregante/content-scripts-register-polyfill#permissions", + ); + } + + if (matches.length === 0) { + throw new Error( + "Type error for parameter contentScriptOptions (Error processing matches: Array requires at least 1 items; you have 0) for contentScripts.register.", + ); + } + + await Promise.all( + matches.map(async (pattern: string) => { + if (!(await chromeProxy.permissions.contains({ origins: [pattern] }))) { + throw new Error(`Permission denied to register a content script for ${pattern}`); + } + }), + ); + + const matchesRegex = patternToRegex(...matches); + const excludeMatchesRegex = patternToRegex( + ...(excludeMatches !== null && excludeMatches !== void 0 ? excludeMatches : []), + ); + const inject = async (url: string, tabId: number, frameId = 0) => { + if ( + !matchesRegex.test(url) || + excludeMatchesRegex.test(url) || + !(await isOriginPermitted(url)) + ) { + return; + } + + await injectContentScript( + { tabId, frameId }, + { css, js, matchAboutBlank, runAt }, + { ignoreTargetErrors: true }, + ); + }; + const tabListener = async ( + tabId: number, + { status }: chrome.tabs.TabChangeInfo, + { url }: chrome.tabs.Tab, + ) => { + if (status === "loading" && url) { + void inject(url, tabId); + } + }; + const navListener = async ({ + tabId, + frameId, + url, + }: chrome.webNavigation.WebNavigationTransitionCallbackDetails) => { + void inject(url, tabId, frameId); + }; + + if (gotNavigation) { + BrowserApi.addListener(chrome.webNavigation.onCommitted, navListener); + } else { + BrowserApi.addListener(chrome.tabs.onUpdated, tabListener); + } + + const registeredContentScript = { + async unregister() { + if (gotNavigation) { + chrome.webNavigation.onCommitted.removeListener(navListener); + } else { + chrome.tabs.onUpdated.removeListener(tabListener); + } + }, + }; + + if (typeof callback === "function") { + callback(registeredContentScript); + } + + return registeredContentScript; + }; +} diff --git a/apps/browser/src/platform/browser/browser-api.spec.ts b/apps/browser/src/platform/browser/browser-api.spec.ts index a1dafb38ec..7e0c61c9d1 100644 --- a/apps/browser/src/platform/browser/browser-api.spec.ts +++ b/apps/browser/src/platform/browser/browser-api.spec.ts @@ -525,29 +525,34 @@ describe("BrowserApi", () => { }); }); - describe("createOffscreenDocument", () => { - it("creates the offscreen document with the supplied reasons and justification", async () => { - const reasons = [chrome.offscreen.Reason.CLIPBOARD]; - const justification = "justification"; + describe("registerContentScriptsMv2", () => { + const details: browser.contentScripts.RegisteredContentScriptOptions = { + matches: [""], + js: [{ file: "content/fido2/page-script.js" }], + }; - await BrowserApi.createOffscreenDocument(reasons, justification); - - expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({ - url: "offscreen-document/index.html", - reasons, - justification, + it("registers content scripts through the `browser.contentScripts` API when the API is available", async () => { + globalThis.browser = mock({ + contentScripts: { register: jest.fn() }, }); + + await BrowserApi.registerContentScriptsMv2(details); + + expect(browser.contentScripts.register).toHaveBeenCalledWith(details); }); - }); - describe("closeOffscreenDocument", () => { - it("closes the offscreen document", () => { - const callbackMock = jest.fn(); + it("registers content scripts through the `registerContentScriptsPolyfill` when the `browser.contentScripts.register` API is not available", async () => { + globalThis.browser = mock({ + contentScripts: { register: undefined }, + }); + jest.spyOn(BrowserApi, "addListener"); - BrowserApi.closeOffscreenDocument(callbackMock); + await BrowserApi.registerContentScriptsMv2(details); - expect(chrome.offscreen.closeDocument).toHaveBeenCalled(); - expect(callbackMock).toHaveBeenCalled(); + expect(BrowserApi.addListener).toHaveBeenCalledWith( + chrome.webNavigation.onCommitted, + expect.any(Function), + ); }); }); }); diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts index b2ee66f051..d0695d53fd 100644 --- a/apps/browser/src/platform/browser/browser-api.ts +++ b/apps/browser/src/platform/browser/browser-api.ts @@ -5,6 +5,8 @@ import { DeviceType } from "@bitwarden/common/enums"; import { TabMessage } from "../../types/tab-messages"; import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service"; +import { registerContentScriptsPolyfill } from "./browser-api.register-content-scripts-polyfill"; + export class BrowserApi { static isWebExtensionsApi: boolean = typeof browser !== "undefined"; static isSafariApi: boolean = @@ -202,10 +204,6 @@ export class BrowserApi { chrome.tabs.sendMessage(tabId, message, options, responseCallback); } - static async getPrivateModeWindows(): Promise { - return (await browser.windows.getAll()).filter((win) => win.incognito); - } - static async onWindowCreated(callback: (win: chrome.windows.Window) => any) { // FIXME: Make sure that is does not cause a memory leak in Safari or use BrowserApi.AddListener // and test that it doesn't break. @@ -236,10 +234,6 @@ export class BrowserApi { return typeof window !== "undefined" && window === BrowserApi.getBackgroundPage(); } - static getApplicationVersion(): string { - return chrome.runtime.getManifest().version; - } - /** * Gets the extension views that match the given properties. This method is not * available within background service worker. As a result, it will return an @@ -565,30 +559,39 @@ export class BrowserApi { } /** - * Opens the offscreen document with the given reasons and justification. + * Handles registration of static content scripts within manifest v2. * - * @param reasons - List of reasons for opening the offscreen document. - * @see https://developer.chrome.com/docs/extensions/reference/api/offscreen#type-Reason - * @param justification - Custom written justification for opening the offscreen document. + * @param contentScriptOptions - Details of the registered content scripts */ - static async createOffscreenDocument(reasons: chrome.offscreen.Reason[], justification: string) { - await chrome.offscreen.createDocument({ - url: "offscreen-document/index.html", - reasons, - justification, - }); + static async registerContentScriptsMv2( + contentScriptOptions: browser.contentScripts.RegisteredContentScriptOptions, + ): Promise { + if (typeof browser !== "undefined" && !!browser.contentScripts?.register) { + return await browser.contentScripts.register(contentScriptOptions); + } + + return await registerContentScriptsPolyfill(contentScriptOptions); } /** - * Closes the offscreen document. + * Handles registration of static content scripts within manifest v3. * - * @param callback - Optional callback to execute after the offscreen document is closed. + * @param scripts - Details of the registered content scripts */ - static closeOffscreenDocument(callback?: () => void) { - chrome.offscreen.closeDocument(() => { - if (callback) { - callback(); - } - }); + static async registerContentScriptsMv3( + scripts: chrome.scripting.RegisteredContentScript[], + ): Promise { + await chrome.scripting.registerContentScripts(scripts); + } + + /** + * Handles unregistering of static content scripts within manifest v3. + * + * @param filter - Optional filter to unregister content scripts. Passing an empty object will unregister all content scripts. + */ + static async unregisterContentScriptsMv3( + filter?: chrome.scripting.ContentScriptFilter, + ): Promise { + await chrome.scripting.unregisterContentScripts(filter); } } diff --git a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts index c5401f8a09..da00bc6fe3 100644 --- a/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts +++ b/apps/browser/src/platform/decorators/dev-flag.decorator.spec.ts @@ -9,7 +9,7 @@ jest.mock("../flags", () => ({ })); class TestClass { - @devFlag("storeSessionDecrypted") test() { + @devFlag("managedEnvironment") test() { return "test"; } } diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts deleted file mode 100644 index 2092f6992b..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { DefaultBrowserStateService } from "../../services/default-browser-state.service"; - -import { browserSession } from "./browser-session.decorator"; -import { SessionStorable } from "./session-storable"; -import { sessionSync } from "./session-sync.decorator"; - -// browserSession initializes SessionSyncers for each sessionSync decorated property -// We don't want to test SessionSyncers, so we'll mock them -jest.mock("./session-syncer"); - -describe("browserSession decorator", () => { - it("should throw if neither StateService nor MemoryStorageService is a constructor argument", () => { - @browserSession - class TestClass {} - expect(() => { - new TestClass(); - }).toThrowError( - "Cannot decorate TestClass with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters", - ); - }); - - it("should create if StateService is a constructor argument", () => { - const stateService = Object.create(DefaultBrowserStateService.prototype, { - memoryStorageService: { - value: Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }), - }, - }); - - @browserSession - class TestClass { - constructor(private stateService: DefaultBrowserStateService) {} - } - - expect(new TestClass(stateService)).toBeDefined(); - }); - - it("should create if MemoryStorageService is a constructor argument", () => { - const memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - - @browserSession - class TestClass { - constructor(private memoryStorageService: AbstractMemoryStorageService) {} - } - - expect(new TestClass(memoryStorageService)).toBeDefined(); - }); - - describe("interaction with @sessionSync decorator", () => { - let memoryStorageService: MemoryStorageService; - - @browserSession - class TestClass { - @sessionSync({ initializer: (s: string) => s }) - private behaviorSubject = new BehaviorSubject(""); - - constructor(private memoryStorageService: MemoryStorageService) {} - - fromJSON(json: any) { - this.behaviorSubject.next(json); - } - } - - beforeEach(() => { - memoryStorageService = Object.create(MemoryStorageService.prototype, { - type: { value: MemoryStorageService.TYPE }, - }); - }); - - it("should create a session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers.length).toEqual(1); - }); - - it("should initialize the session syncer", () => { - const testClass = new TestClass(memoryStorageService) as any as SessionStorable; - expect(testClass.__sessionSyncers[0].init).toHaveBeenCalled(); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts deleted file mode 100644 index 8cf84ef153..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/browser-session.decorator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Constructor } from "type-fest"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; - -import { SessionStorable } from "./session-storable"; -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -/** - * Mark the class as syncing state across the browser session. This decorator finds rxjs BehaviorSubject properties - * marked with @sessionSync and syncs these values across the browser session. - * - * @param constructor - * @returns A new constructor that extends the original one to add session syncing. - */ -export function browserSession>(constructor: TCtor) { - return class extends constructor implements SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; - - constructor(...args: any[]) { - super(...args); - - // Require state service to be injected - const storageService: AbstractMemoryStorageService = this.findStorageService( - [this as any].concat(args), - ); - - if (this.__syncedItemMetadata == null || !(this.__syncedItemMetadata instanceof Array)) { - return; - } - - this.__sessionSyncers = this.__syncedItemMetadata.map((metadata) => - this.buildSyncer(metadata, storageService), - ); - } - - buildSyncer(metadata: SyncedItemMetadata, storageSerice: AbstractMemoryStorageService) { - const syncer = new SessionSyncer( - (this as any)[metadata.propertyKey], - storageSerice, - metadata, - ); - // 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 - syncer.init(); - return syncer; - } - - findStorageService(args: any[]): AbstractMemoryStorageService { - const storageService = args.find(this.isMemoryStorageService); - - if (storageService) { - return storageService; - } - - const stateService = args.find( - (arg) => - arg?.memoryStorageService != null && - this.isMemoryStorageService(arg.memoryStorageService), - ); - if (stateService) { - return stateService.memoryStorageService; - } - - throw new Error( - `Cannot decorate ${constructor.name} with browserSession, Browser's AbstractMemoryStorageService must be accessible through the observed classes parameters`, - ); - } - - isMemoryStorageService(arg: any): arg is AbstractMemoryStorageService { - return arg.type != null && arg.type === AbstractMemoryStorageService.TYPE; - } - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/index.ts b/apps/browser/src/platform/decorators/session-sync-observable/index.ts deleted file mode 100644 index c0c547192e..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { browserSession } from "./browser-session.decorator"; -export { sessionSync } from "./session-sync.decorator"; diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts deleted file mode 100644 index f5838b86ef..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-storable.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export interface SessionStorable { - __syncedItemMetadata: SyncedItemMetadata[]; - __sessionSyncers: SessionSyncer[]; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts deleted file mode 100644 index 7a6e726608..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { BehaviorSubject } from "rxjs"; - -import { sessionSync } from "./session-sync.decorator"; - -describe("sessionSync decorator", () => { - const initializer = (s: string) => "test"; - class TestClass { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - @sessionSync({ initializer: initializer, initializeAs: "array" }) - private secondTestProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - this.secondTestProperty.complete(); - } - } - - it("should add __syncedItemKeys to prototype", () => { - const testClass = new TestClass(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_0", - initializer: initializer, - }), - expect.objectContaining({ - propertyKey: "secondTestProperty", - sessionKey: "secondTestProperty_1", - initializer: initializer, - initializeAs: "array", - }), - ]); - testClass.complete(); - }); - - class TestClass2 { - @sessionSync({ initializer: initializer }) - private testProperty = new BehaviorSubject(""); - - complete() { - this.testProperty.complete(); - } - } - - it("should maintain sessionKey index count for other test classes", () => { - const testClass = new TestClass2(); - expect((testClass as any).__syncedItemMetadata).toEqual([ - expect.objectContaining({ - propertyKey: "testProperty", - sessionKey: "testProperty_2", - initializer: initializer, - }), - ]); - testClass.complete(); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts deleted file mode 100644 index e439cea45a..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-sync.decorator.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { SessionStorable } from "./session-storable"; -import { InitializeOptions } from "./sync-item-metadata"; - -class BuildOptions> { - initializer?: (keyValuePair: TJson) => T; - initializeAs?: InitializeOptions; -} - -// Used to ensure uniqueness for each synced observable -let index = 0; - -/** - * A decorator used to indicate the BehaviorSubject should be synced for this browser session across all contexts. - * - * >**Note** This decorator does nothing if the enclosing class is not decorated with @browserSession. - * - * >**Note** The Behavior subject must be initialized with a default or in the constructor of the class. If it is not, an error will be thrown. - * - * >**!!Warning!!** If the property is overwritten at any time, the new value will not be synced across the browser session. - * - * @param buildOptions - * Builders for the value, requires either a constructor (ctor) for your BehaviorSubject type or an - * initializer function that takes a key value pair representation of the BehaviorSubject data - * and returns your instantiated BehaviorSubject value. `initializeAs can optionally be used to indicate - * the provided initializer function should be used to build an array of values. For example, - * ```ts - * \@sessionSync({ initializer: Foo.fromJSON, initializeAs: 'array' }) - * ``` - * is equivalent to - * ``` - * \@sessionSync({ initializer: (obj: any[]) => obj.map((f) => Foo.fromJSON }) - * ``` - * - * @returns decorator function - */ -export function sessionSync(buildOptions: BuildOptions) { - return (prototype: unknown, propertyKey: string) => { - // Force prototype into SessionStorable and implement it. - const p = prototype as SessionStorable; - - if (p.__syncedItemMetadata == null) { - p.__syncedItemMetadata = []; - } - - p.__syncedItemMetadata.push({ - propertyKey, - sessionKey: `${propertyKey}_${index++}`, - initializer: buildOptions.initializer, - initializeAs: buildOptions.initializeAs ?? "object", - }); - }; -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts deleted file mode 100644 index 18f0ceac60..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.spec.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { awaitAsync } from "@bitwarden/common/../spec/utils"; -import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, ReplaySubject } from "rxjs"; - -import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SessionSyncer } from "./session-syncer"; -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("session syncer", () => { - const propertyKey = "behaviorSubject"; - const sessionKey = "Test__" + propertyKey; - const metaData: SyncedItemMetadata = { - propertyKey, - sessionKey, - initializer: (s: string) => s, - initializeAs: "object", - }; - let storageService: MockProxy; - let sut: SessionSyncer; - let behaviorSubject: BehaviorSubject; - - beforeEach(() => { - behaviorSubject = new BehaviorSubject(""); - jest.spyOn(chrome.runtime, "getManifest").mockReturnValue({ - name: "bitwarden-test", - version: "0.0.0", - manifest_version: 3, - }); - - storageService = mock(); - storageService.has.mockResolvedValue(false); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - }); - - afterEach(() => { - jest.resetAllMocks(); - - behaviorSubject.complete(); - }); - - describe("constructor", () => { - it("should throw if subject is not an instance of Subject", () => { - expect(() => { - new SessionSyncer({} as any, storageService, null); - }).toThrowError("subject must inherit from Subject"); - }); - - it("should create if either ctor or initializer is provided", () => { - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: () => null, - }), - ).toBeDefined(); - expect( - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializer: (s: any) => s, - initializeAs: "object", - }), - ).toBeDefined(); - }); - it("should throw if neither ctor or initializer is provided", () => { - expect(() => { - new SessionSyncer(behaviorSubject, storageService, { - propertyKey, - sessionKey, - initializeAs: "object", - initializer: null, - }); - }).toThrowError("initializer must be provided"); - }); - }); - - describe("init", () => { - it("should ignore all updates currently in a ReplaySubject's buffer", () => { - const replaySubject = new ReplaySubject(Infinity); - replaySubject.next("1"); - replaySubject.next("2"); - replaySubject.next("3"); - sut = new SessionSyncer(replaySubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(3); - }); - - it("should ignore BehaviorSubject's initial value", () => { - const behaviorSubject = new BehaviorSubject("initial"); - sut = new SessionSyncer(behaviorSubject, storageService, metaData); - // block observing the subject - jest.spyOn(sut as any, "observe").mockImplementation(); - - // 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 - sut.init(); - - expect(sut["ignoreNUpdates"]).toBe(1); - }); - - it("should grab an initial value from storage if it exists", async () => { - storageService.has.mockResolvedValue(true); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "updateFromMemory").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).toHaveBeenCalledWith(); - }); - - it("should not grab an initial value from storage if it does not exist", async () => { - storageService.has.mockResolvedValue(false); - //Block a call to update - const updateSpy = jest.spyOn(sut as any, "update").mockImplementation(); - - // 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 - sut.init(); - await awaitAsync(); - - expect(updateSpy).not.toHaveBeenCalled(); - }); - }); - - describe("a value is emitted on the observable", () => { - let sendMessageSpy: jest.SpyInstance; - const value = "test"; - const serializedValue = JSON.stringify(value); - - beforeEach(() => { - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - - behaviorSubject.next(value); - }); - - it("should update sessionSyncers in other contexts", async () => { - // await finishing of fire-and-forget operation - await new Promise((resolve) => setTimeout(resolve, 100)); - - expect(sendMessageSpy).toHaveBeenCalledTimes(1); - expect(sendMessageSpy).toHaveBeenCalledWith(`${sessionKey}_update`, { - id: sut.id, - serializedValue, - }); - }); - }); - - describe("A message is received", () => { - let nextSpy: jest.SpyInstance; - let sendMessageSpy: jest.SpyInstance; - - beforeEach(() => { - nextSpy = jest.spyOn(behaviorSubject, "next"); - sendMessageSpy = jest.spyOn(BrowserApi, "sendMessage"); - - // 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 - sut.init(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should ignore messages with the wrong command", async () => { - await sut.updateFromMessage({ command: "wrong_command", id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should ignore messages from itself", async () => { - await sut.updateFromMessage({ command: `${sessionKey}_update`, id: sut.id }); - - expect(storageService.getBypassCache).not.toHaveBeenCalled(); - expect(nextSpy).not.toHaveBeenCalled(); - }); - - it("should update from message on emit from another instance", async () => { - const builder = jest.fn(); - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - const value = "test"; - const serializedValue = JSON.stringify(value); - builder.mockReturnValue(value); - - // Expect no circular messaging - await awaitAsync(); - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(storageService.getBypassCache).toHaveBeenCalledTimes(0); - - expect(nextSpy).toHaveBeenCalledTimes(1); - expect(nextSpy).toHaveBeenCalledWith(value); - expect(behaviorSubject.value).toBe(value); - - // Expect no circular messaging - expect(sendMessageSpy).toHaveBeenCalledTimes(0); - }); - }); - - describe("memory storage", () => { - const value = "test"; - const serializedValue = JSON.stringify(value); - let saveSpy: jest.SpyInstance; - const builder = jest.fn().mockReturnValue(value); - const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); - const isBackgroundPageSpy = jest.spyOn(BrowserApi, "isBackgroundPage"); - - beforeEach(async () => { - jest.spyOn(SyncedItemMetadata, "builder").mockReturnValue(builder); - saveSpy = jest.spyOn(storageService, "save"); - - // 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 - sut.init(); - await awaitAsync(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it("should always store on observed next for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(2); - }); - - it("should not store on message receive for manifest version 3", async () => { - manifestVersionSpy.mockReturnValue(3); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(0); - }); - - it("should store on message receive for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - await sut.updateFromMessage({ - command: `${sessionKey}_update`, - id: "different_id", - serializedValue, - }); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - - it("should store on observed next for manifest version 2 for background page only", async () => { - manifestVersionSpy.mockReturnValue(2); - isBackgroundPageSpy.mockReturnValueOnce(true).mockReturnValueOnce(false); - behaviorSubject.next(value); - await awaitAsync(); - behaviorSubject.next(value); - await awaitAsync(); - - expect(saveSpy).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts b/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts deleted file mode 100644 index 6561d5074c..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/session-syncer.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { BehaviorSubject, concatMap, ReplaySubject, skip, Subject, Subscription } from "rxjs"; - -import { AbstractMemoryStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; - -import { BrowserApi } from "../../browser/browser-api"; - -import { SyncedItemMetadata } from "./sync-item-metadata"; - -export class SessionSyncer { - subscription: Subscription; - id = Utils.newGuid(); - - // ignore initial values - private ignoreNUpdates = 0; - - constructor( - private subject: Subject, - private memoryStorageService: AbstractMemoryStorageService, - private metaData: SyncedItemMetadata, - ) { - if (!(subject instanceof Subject)) { - throw new Error("subject must inherit from Subject"); - } - - if (metaData.initializer == null) { - throw new Error("initializer must be provided"); - } - } - - async init() { - switch (this.subject.constructor) { - case ReplaySubject: - // ignore all updates currently in the buffer - this.ignoreNUpdates = (this.subject as any)._buffer.length; - break; - case BehaviorSubject: - this.ignoreNUpdates = 1; - break; - default: - break; - } - - await this.observe(); - // must be synchronous - const hasInSessionMemory = await this.memoryStorageService.has(this.metaData.sessionKey); - if (hasInSessionMemory) { - await this.updateFromMemory(); - } - - this.listenForUpdates(); - } - - private async observe() { - const stream = this.subject.pipe(skip(this.ignoreNUpdates)); - this.ignoreNUpdates = 0; - - // This may be a memory leak. - // There is no good time to unsubscribe from this observable. Hopefully Manifest V3 clears memory from temporary - // contexts. If so, this is handled by destruction of the context. - this.subscription = stream - .pipe( - concatMap(async (next) => { - if (this.ignoreNUpdates > 0) { - this.ignoreNUpdates -= 1; - return; - } - await this.updateSession(next); - }), - ) - .subscribe(); - } - - private listenForUpdates() { - // This is an unawaited promise, but it will be executed asynchronously in the background. - BrowserApi.messageListener(this.updateMessageCommand, (message) => { - // 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.updateFromMessage(message); - }); - } - - async updateFromMessage(message: any) { - if (message.command != this.updateMessageCommand || message.id === this.id) { - return; - } - await this.update(message.serializedValue); - } - - async updateFromMemory() { - const value = await this.memoryStorageService.getBypassCache(this.metaData.sessionKey); - await this.update(value); - } - - async update(serializedValue: any) { - if (!serializedValue) { - return; - } - - const unBuiltValue = JSON.parse(serializedValue); - if (!BrowserApi.isManifestVersion(3) && BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - const builder = SyncedItemMetadata.builder(this.metaData); - const value = builder(unBuiltValue); - this.ignoreNUpdates = 1; - this.subject.next(value); - } - - private async updateSession(value: any) { - if (!value) { - return; - } - - const serializedValue = JSON.stringify(value); - if (BrowserApi.isManifestVersion(3) || BrowserApi.isBackgroundPage(self)) { - await this.memoryStorageService.save(this.metaData.sessionKey, serializedValue); - } - await BrowserApi.sendMessage(this.updateMessageCommand, { id: this.id, serializedValue }); - } - - private get updateMessageCommand() { - return `${this.metaData.sessionKey}_update`; - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts b/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts deleted file mode 100644 index fe2b393923..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/sync-item-metadata.ts +++ /dev/null @@ -1,25 +0,0 @@ -export type InitializeOptions = "array" | "record" | "object"; - -export class SyncedItemMetadata { - propertyKey: string; - sessionKey: string; - initializer: (keyValuePair: any) => any; - initializeAs: InitializeOptions; - - static builder(metadata: SyncedItemMetadata): (o: any) => any { - const itemBuilder = metadata.initializer; - if (metadata.initializeAs === "array") { - return (keyValuePair: any) => keyValuePair.map((o: any) => itemBuilder(o)); - } else if (metadata.initializeAs === "record") { - return (keyValuePair: any) => { - const record: Record = {}; - for (const key in keyValuePair) { - record[key] = itemBuilder(keyValuePair[key]); - } - return record; - }; - } else { - return (keyValuePair: any) => itemBuilder(keyValuePair); - } - } -} diff --git a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts b/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts deleted file mode 100644 index 61eb63eaac..0000000000 --- a/apps/browser/src/platform/decorators/session-sync-observable/synced-item-metadata.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { SyncedItemMetadata } from "./sync-item-metadata"; - -describe("builder", () => { - const propertyKey = "propertyKey"; - const key = "key"; - const initializer = (s: any) => "used initializer"; - - it("should use initializer", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer, - initializeAs: "object", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({})).toBe("used initializer"); - }); - - it("should honor initialize as array", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "array", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder([{}])).toBeInstanceOf(Array); - expect(builder([{}])[0]).toBe("used initializer"); - }); - - it("should honor initialize as record", () => { - const metadata: SyncedItemMetadata = { - propertyKey, - sessionKey: key, - initializer: initializer, - initializeAs: "record", - }; - const builder = SyncedItemMetadata.builder(metadata); - expect(builder({ key: "" })).toBeInstanceOf(Object); - expect(builder({ key: "" })).toStrictEqual({ key: "used initializer" }); - }); -}); diff --git a/apps/browser/src/platform/flags.ts b/apps/browser/src/platform/flags.ts index 36aa698a7b..383e982f06 100644 --- a/apps/browser/src/platform/flags.ts +++ b/apps/browser/src/platform/flags.ts @@ -19,7 +19,6 @@ export type Flags = { // required to avoid linting errors when there are no flags // eslint-disable-next-line @typescript-eslint/ban-types export type DevFlags = { - storeSessionDecrypted?: boolean; managedEnvironment?: GroupPolicyEnvironment; } & SharedDevFlags; diff --git a/apps/browser/src/platform/listeners/on-install-listener.ts b/apps/browser/src/platform/listeners/on-install-listener.ts index ef206301e3..adf575a17a 100644 --- a/apps/browser/src/platform/listeners/on-install-listener.ts +++ b/apps/browser/src/platform/listeners/on-install-listener.ts @@ -23,6 +23,11 @@ export async function onInstallListener(details: chrome.runtime.InstalledDetails stateServiceOptions: { stateFactory: new StateFactory(GlobalState, Account), }, + platformUtilsServiceOptions: { + win: self, + biometricCallback: async () => false, + clipboardWriteCallback: async () => {}, + }, }; const environmentService = await environmentServiceFactory(cache, opts); diff --git a/apps/browser/src/platform/messaging/chrome-message.sender.ts b/apps/browser/src/platform/messaging/chrome-message.sender.ts new file mode 100644 index 0000000000..0e57ecfb4e --- /dev/null +++ b/apps/browser/src/platform/messaging/chrome-message.sender.ts @@ -0,0 +1,37 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CommandDefinition, MessageSender } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +type ErrorHandler = (logger: LogService, command: string) => void; + +const HANDLED_ERRORS: Record = { + "Could not establish connection. Receiving end does not exist.": (logger, command) => + logger.debug(`Receiving end didn't exist for command '${command}'`), + + "The message port closed before a response was received.": (logger, command) => + logger.debug(`Port was closed for command '${command}'`), +}; + +export class ChromeMessageSender implements MessageSender { + constructor(private readonly logService: LogService) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + chrome.runtime.sendMessage(Object.assign(payload, { command: command }), () => { + if (chrome.runtime.lastError) { + const errorHandler = HANDLED_ERRORS[chrome.runtime.lastError.message]; + if (errorHandler != null) { + errorHandler(this.logService, command); + return; + } + + this.logService.warning( + `Unhandled error while sending message with command '${command}': ${chrome.runtime.lastError.message}`, + ); + } + }); + } +} diff --git a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts index e5aa8c86f5..2a67d55c96 100644 --- a/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/abstractions/offscreen-document.ts @@ -1,7 +1,8 @@ -type OffscreenDocumentExtensionMessage = { +export type OffscreenDocumentExtensionMessage = { [key: string]: any; command: string; text?: string; + decryptRequest?: string; }; type OffscreenExtensionMessageEventParams = { @@ -9,18 +10,21 @@ type OffscreenExtensionMessageEventParams = { sender: chrome.runtime.MessageSender; }; -type OffscreenDocumentExtensionMessageHandlers = { +export type OffscreenDocumentExtensionMessageHandlers = { [key: string]: ({ message, sender }: OffscreenExtensionMessageEventParams) => any; offscreenCopyToClipboard: ({ message }: OffscreenExtensionMessageEventParams) => any; offscreenReadFromClipboard: () => any; + offscreenDecryptItems: ({ message }: OffscreenExtensionMessageEventParams) => Promise; }; -interface OffscreenDocument { +export interface OffscreenDocument { init(): void; } -export { - OffscreenDocumentExtensionMessage, - OffscreenDocumentExtensionMessageHandlers, - OffscreenDocument, -}; +export abstract class OffscreenDocumentService { + abstract withDocument( + reasons: chrome.offscreen.Reason[], + justification: string, + callback: () => Promise | T, + ): Promise; +} diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts new file mode 100644 index 0000000000..d6be0a924e --- /dev/null +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.spec.ts @@ -0,0 +1,101 @@ +import { DefaultOffscreenDocumentService } from "./offscreen-document.service"; + +class TestCase { + synchronicity: string; + private _callback: () => Promise | any; + get callback() { + return jest.fn(this._callback); + } + + constructor(synchronicity: string, callback: () => Promise | any) { + this.synchronicity = synchronicity; + this._callback = callback; + } + + toString() { + return this.synchronicity; + } +} + +describe.each([ + new TestCase("synchronous callback", () => 42), + new TestCase("asynchronous callback", () => Promise.resolve(42)), +])("DefaultOffscreenDocumentService %s", (testCase) => { + let sut: DefaultOffscreenDocumentService; + const reasons = [chrome.offscreen.Reason.TESTING]; + const justification = "justification is testing"; + const url = "offscreen-document/index.html"; + const api = { + createDocument: jest.fn(), + closeDocument: jest.fn(), + hasDocument: jest.fn().mockResolvedValue(false), + Reason: chrome.offscreen.Reason, + }; + let callback: jest.Mock<() => Promise | number>; + + beforeEach(() => { + callback = testCase.callback; + chrome.offscreen = api; + + sut = new DefaultOffscreenDocumentService(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("withDocument", () => { + it("creates a document when none exists", async () => { + await sut.withDocument(reasons, justification, () => {}); + + expect(chrome.offscreen.createDocument).toHaveBeenCalledWith({ + url, + reasons, + justification, + }); + }); + + it("does not create a document when one exists", async () => { + api.hasDocument.mockResolvedValue(true); + + await sut.withDocument(reasons, justification, callback); + + expect(chrome.offscreen.createDocument).not.toHaveBeenCalled(); + }); + + describe.each([true, false])("hasDocument returns %s", (hasDocument) => { + beforeEach(() => { + api.hasDocument.mockResolvedValue(hasDocument); + }); + + it("calls the callback", async () => { + await sut.withDocument(reasons, justification, callback); + + expect(callback).toHaveBeenCalled(); + }); + + it("returns the callback result", async () => { + const result = await sut.withDocument(reasons, justification, callback); + + expect(result).toBe(42); + }); + + it("closes the document when the callback completes and no other callbacks are running", async () => { + await sut.withDocument(reasons, justification, callback); + + expect(chrome.offscreen.closeDocument).toHaveBeenCalled(); + }); + + it("does not close the document when the callback completes and other callbacks are running", async () => { + await Promise.all([ + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + sut.withDocument(reasons, justification, callback), + ]); + + expect(chrome.offscreen.closeDocument).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts new file mode 100644 index 0000000000..da0ca38269 --- /dev/null +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.service.ts @@ -0,0 +1,41 @@ +export class DefaultOffscreenDocumentService implements DefaultOffscreenDocumentService { + private workerCount = 0; + + constructor() {} + + async withDocument( + reasons: chrome.offscreen.Reason[], + justification: string, + callback: () => Promise | T, + ): Promise { + this.workerCount++; + try { + if (!(await this.documentExists())) { + await this.create(reasons, justification); + } + + return await callback(); + } finally { + this.workerCount--; + if (this.workerCount === 0) { + await this.close(); + } + } + } + + private async create(reasons: chrome.offscreen.Reason[], justification: string): Promise { + await chrome.offscreen.createDocument({ + url: "offscreen-document/index.html", + reasons, + justification, + }); + } + + private async close(): Promise { + await chrome.offscreen.closeDocument(); + } + + private async documentExists(): Promise { + return await chrome.offscreen.hasDocument(); + } +} 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 1cbcc7a94c..9d3cadbba8 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.spec.ts @@ -1,7 +1,25 @@ +import { mock } from "jest-mock-extended"; + +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; + import { flushPromises, sendExtensionRuntimeMessage } from "../../autofill/spec/testing-utils"; import { BrowserApi } from "../browser/browser-api"; import BrowserClipboardService from "../services/browser-clipboard.service"; +jest.mock( + "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation", + () => ({ + MultithreadEncryptServiceImplementation: class MultithreadEncryptServiceImplementation { + getDecryptedItemsFromWorker = async ( + items: Decryptable[], + _key: SymmetricCryptoKey, + ): Promise => JSON.stringify(items); + }, + }), +); + describe("OffscreenDocument", () => { const browserApiMessageListenerSpy = jest.spyOn(BrowserApi, "messageListener"); const browserClipboardServiceCopySpy = jest.spyOn(BrowserClipboardService, "copy"); @@ -28,6 +46,7 @@ describe("OffscreenDocument", () => { }); it("shows a console message if the handler throws an error", async () => { + const error = new Error("test error"); browserClipboardServiceCopySpy.mockRejectedValueOnce(new Error("test error")); sendExtensionRuntimeMessage({ command: "offscreenCopyToClipboard", text: "test" }); @@ -35,7 +54,8 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceCopySpy).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith( - "Error resolving extension message response: Error: test error", + "Error resolving extension message response", + error, ); }); @@ -58,5 +78,37 @@ describe("OffscreenDocument", () => { expect(browserClipboardServiceReadSpy).toHaveBeenCalledWith(window); }); }); + + describe("handleOffscreenDecryptItems", () => { + it("returns an empty array as a string if the decrypt request is not present in the message", async () => { + let response: string | undefined; + sendExtensionRuntimeMessage( + { command: "offscreenDecryptItems" }, + mock(), + (res: string) => (response = res), + ); + await flushPromises(); + + expect(response).toBe("[]"); + }); + + it("decrypts the items and sends back the response as a string", async () => { + const items = [{ id: "test" }]; + const key = { id: "test" }; + const decryptRequest = JSON.stringify({ items, key }); + let response: string | undefined; + + sendExtensionRuntimeMessage( + { command: "offscreenDecryptItems", decryptRequest }, + mock(), + (res: string) => { + response = res; + }, + ); + await flushPromises(); + + expect(response).toBe(JSON.stringify(items)); + }); + }); }); }); diff --git a/apps/browser/src/platform/offscreen-document/offscreen-document.ts b/apps/browser/src/platform/offscreen-document/offscreen-document.ts index 627036b80b..509193d5ee 100644 --- a/apps/browser/src/platform/offscreen-document/offscreen-document.ts +++ b/apps/browser/src/platform/offscreen-document/offscreen-document.ts @@ -1,21 +1,35 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { BrowserApi } from "../browser/browser-api"; import BrowserClipboardService from "../services/browser-clipboard.service"; import { + OffscreenDocument as OffscreenDocumentInterface, OffscreenDocumentExtensionMessage, OffscreenDocumentExtensionMessageHandlers, - OffscreenDocument as OffscreenDocumentInterface, } from "./abstractions/offscreen-document"; class OffscreenDocument implements OffscreenDocumentInterface { - private consoleLogService: ConsoleLogService = new ConsoleLogService(false); + private readonly consoleLogService: ConsoleLogService; + private encryptService: MultithreadEncryptServiceImplementation; private readonly extensionMessageHandlers: OffscreenDocumentExtensionMessageHandlers = { offscreenCopyToClipboard: ({ message }) => this.handleOffscreenCopyToClipboard(message), offscreenReadFromClipboard: () => this.handleOffscreenReadFromClipboard(), + offscreenDecryptItems: ({ message }) => this.handleOffscreenDecryptItems(message), }; + constructor() { + const cryptoFunctionService = new WebCryptoFunctionService(self); + this.consoleLogService = new ConsoleLogService(false); + this.encryptService = new MultithreadEncryptServiceImplementation( + cryptoFunctionService, + this.consoleLogService, + true, + ); + } + /** * Initializes the offscreen document extension. */ @@ -39,6 +53,23 @@ class OffscreenDocument implements OffscreenDocumentInterface { return await BrowserClipboardService.read(self); } + /** + * Decrypts the items in the message using the encrypt service. + * + * @param message - The extension message containing the items to decrypt + */ + private async handleOffscreenDecryptItems( + message: OffscreenDocumentExtensionMessage, + ): Promise { + const { decryptRequest } = message; + if (!decryptRequest) { + return "[]"; + } + + const request = JSON.parse(decryptRequest); + return await this.encryptService.getDecryptedItemsFromWorker(request.items, request.key); + } + /** * Sets up the listener for extension messages. */ @@ -71,7 +102,7 @@ class OffscreenDocument implements OffscreenDocumentInterface { Promise.resolve(messageResponse) .then((response) => sendResponse(response)) .catch((error) => - this.consoleLogService.error(`Error resolving extension message response: ${error}`), + this.consoleLogService.error("Error resolving extension message response", error), ); return true; }; diff --git a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts index e84cd19a45..73f0d23f4f 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.spec.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.spec.ts @@ -138,28 +138,6 @@ describe("BrowserPopupUtils", () => { }); }); - describe("inPrivateMode", () => { - it("returns false if the background requires initialization", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(false); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(false); - }); - - it("returns false if the manifest version is for version 3", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true); - jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(3); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(false); - }); - - it("returns true if the background does not require initalization and the manifest version is version 2", () => { - jest.spyOn(BrowserPopupUtils, "backgroundInitializationRequired").mockReturnValue(true); - jest.spyOn(BrowserApi, "manifestVersion", "get").mockReturnValue(2); - - expect(BrowserPopupUtils.inPrivateMode()).toBe(true); - }); - }); - describe("openPopout", () => { beforeEach(() => { jest.spyOn(BrowserApi, "getWindow").mockResolvedValueOnce({ @@ -203,7 +181,7 @@ describe("BrowserPopupUtils", () => { expect(BrowserPopupUtils["buildPopoutUrl"]).not.toHaveBeenCalled(); }); - it("replaces any existing `uilocation=` query params within the passed extension url path to state the the uilocaiton is a popup", async () => { + it("replaces any existing `uilocation=` query params within the passed extension url path to state the uilocation is a popup", async () => { const url = "popup/index.html?uilocation=sidebar#/tabs/vault"; jest.spyOn(BrowserPopupUtils as any, "isSingleActionPopoutOpen").mockResolvedValueOnce(false); diff --git a/apps/browser/src/platform/popup/browser-popup-utils.ts b/apps/browser/src/platform/popup/browser-popup-utils.ts index 2e087db83e..a2249d466c 100644 --- a/apps/browser/src/platform/popup/browser-popup-utils.ts +++ b/apps/browser/src/platform/popup/browser-popup-utils.ts @@ -89,13 +89,6 @@ class BrowserPopupUtils { return !BrowserApi.getBackgroundPage(); } - /** - * Identifies if the popup is loading in private mode. - */ - static inPrivateMode() { - return BrowserPopupUtils.backgroundInitializationRequired() && !BrowserApi.isManifestVersion(3); - } - /** * Opens a popout window of any extension page. If the popout window is already open, it will be focused. * diff --git a/apps/browser/src/platform/popup/header.component.ts b/apps/browser/src/platform/popup/header.component.ts index ebda12c2a4..1373837866 100644 --- a/apps/browser/src/platform/popup/header.component.ts +++ b/apps/browser/src/platform/popup/header.component.ts @@ -1,10 +1,8 @@ import { Component, Input } from "@angular/core"; -import { Observable, combineLatest, map, of, switchMap } from "rxjs"; +import { Observable, map, of, switchMap } 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 { UserId } from "@bitwarden/common/types/guid"; import { enableAccountSwitching } from "../flags"; @@ -16,18 +14,15 @@ export class HeaderComponent { @Input() noTheme = false; @Input() hideAccountSwitcher = false; authedAccounts$: Observable; - constructor(accountService: AccountService, authService: AuthService) { - this.authedAccounts$ = accountService.accounts$.pipe( - switchMap((accounts) => { + constructor(authService: AuthService) { + this.authedAccounts$ = authService.authStatuses$.pipe( + map((record) => Object.values(record)), + switchMap((statuses) => { if (!enableAccountSwitching()) { return of(false); } - return combineLatest( - Object.keys(accounts).map((id) => authService.authStatusFor$(id as UserId)), - ).pipe( - map((statuses) => statuses.some((status) => status !== AuthenticationStatus.LoggedOut)), - ); + return of(statuses.some((status) => status !== AuthenticationStatus.LoggedOut)); }), ); } diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html new file mode 100644 index 0000000000..2cbbca79c0 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -0,0 +1,9 @@ +
+
+
+ +
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.ts b/apps/browser/src/platform/popup/layout/popup-footer.component.ts new file mode 100644 index 0000000000..826a1d1c60 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.ts @@ -0,0 +1,9 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-footer", + templateUrl: "popup-footer.component.html", + standalone: true, + imports: [], +}) +export class PopupFooterComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html new file mode 100644 index 0000000000..c0894f8168 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -0,0 +1,19 @@ +
+
+
+ +

{{ pageTitle }}

+
+
+ +
+
+
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.ts b/apps/browser/src/platform/popup/layout/popup-header.component.ts new file mode 100644 index 0000000000..f2f8eb95af --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -0,0 +1,34 @@ +import { BooleanInput, coerceBooleanProperty } from "@angular/cdk/coercion"; +import { CommonModule, Location } from "@angular/common"; +import { Component, Input } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { IconButtonModule, TypographyModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-header", + templateUrl: "popup-header.component.html", + standalone: true, + imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule], +}) +export class PopupHeaderComponent { + /** Display the back button, which uses Location.back() to go back one page in history */ + @Input() + get showBackButton() { + return this._showBackButton; + } + set showBackButton(value: BooleanInput) { + this._showBackButton = coerceBooleanProperty(value); + } + + private _showBackButton = false; + + /** Title string that will be inserted as an h1 */ + @Input({ required: true }) pageTitle: string; + + constructor(private location: Location) {} + + back() { + this.location.back(); + } +} diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx new file mode 100644 index 0000000000..91f7dab277 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx @@ -0,0 +1,138 @@ +import { Meta, Story, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./popup-layout.stories"; + + + +Please note that because these stories use `router-outlet`, there are issues with rendering content +when Light & Dark mode is selected. The stories are best viewed by selecting one color mode. + +# Popup Tab Navigation + +The popup tab navigation component composes together the popup page and the bottom tab navigation +footer. This component is intended to be used a level _above_ each extension tab's page code. + +The navigation footer contains the 4 main page links for the browser extension. It uses the Angular +router to determine which page is currently active, and style the button appropriately. Clicking on +the buttons will navigate to the correct route. The navigation footer has a max-width built in so +that the page looks nice when the extension is popped out. + +Long button names will be ellipsed. + +Usage example: + +```html + + + +``` + +# Popup Page + +The popup page handles positioning a page's `header` and `footer` elements, and inserting the rest +of the content into the `main` element with scroll. There is also a max-width built in so that the +page looks nice when the extension is popped out. + +**Slots** + +- `header` + - Use `popup-header` component. + - Every page should have a header. +- `footer` + - Use the `popup-footer` component. + - Not every page will have a footer. +- default + - Whatever content you want in `main`. + +Basic usage example: + +```html + + +
This is content
+ +
+``` + +## Popup header + +**Args** + +- `pageTitle`: required + - Inserts title as an `h1`. +- `showBackButton`: optional, defaults to `false` + - Toggles the back button to appear. The back button uses `Location.back()` to navigate back one + page in history. + +**Slots** + +- `end` + - Use to insert one or more interactive elements. + - The header handles the spacing between elements passed to the `end` slot. + +Usage example: + +```html + + + + + + +``` + +Common interactive elements to insert into the `end` slot are: + +- `app-current-account`: shows current account and switcher +- `app-pop-out`: shows popout button when the extension is not already popped out +- "Add" button: this can be accomplished with the Button component and any custom functionality for + that particular page + +## Popup footer + +Popup footer should be used when the page displays action buttons. It functions similarly to the +Dialog footer in that the calling code is responsible for passing in the different buttons that need +to be rendered. + +Usage example: + +```html + + + + +``` + +# Page types + +There are a few types of pages that are used in the browser extension. + +View the story source code to see examples of how to construct these types of pages. + +## Extension Tab + +Example of wrapping an extension page in the `popup-tab-navigation` component. + + + + + +## Extension Page + +Examples of using just the `popup-page` component, without and with a footer. + + + + + + + + + +## Popped out + +When the browser extension is popped out, the "popout" button should not be passed to the header. + + + + diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts new file mode 100644 index 0000000000..28692c79e1 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -0,0 +1,380 @@ +import { CommonModule } from "@angular/common"; +import { Component, importProvidersFrom } from "@angular/core"; +import { RouterModule } from "@angular/router"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { + AvatarModule, + BadgeModule, + ButtonModule, + I18nMockService, + IconButtonModule, + ItemModule, +} from "@bitwarden/components"; + +import { PopupFooterComponent } from "./popup-footer.component"; +import { PopupHeaderComponent } from "./popup-header.component"; +import { PopupPageComponent } from "./popup-page.component"; +import { PopupTabNavigationComponent } from "./popup-tab-navigation.component"; + +@Component({ + selector: "extension-container", + template: ` +
+ +
+ `, + standalone: true, +}) +class ExtensionContainerComponent {} + +@Component({ + selector: "vault-placeholder", + template: ` + + + + + + + + + + + + + + + + + + `, + standalone: true, + imports: [CommonModule, ItemModule, BadgeModule, IconButtonModule], +}) +class VaultComponent { + protected data = Array.from(Array(20).keys()); +} + +@Component({ + selector: "generator-placeholder", + template: `
generator stuff here
`, + standalone: true, +}) +class GeneratorComponent {} + +@Component({ + selector: "send-placeholder", + template: `
send some stuff
`, + standalone: true, +}) +class SendComponent {} + +@Component({ + selector: "settings-placeholder", + template: `
change your settings
`, + standalone: true, +}) +class SettingsComponent {} + +@Component({ + selector: "mock-add-button", + template: ` + + `, + standalone: true, + imports: [ButtonModule], +}) +class MockAddButtonComponent {} + +@Component({ + selector: "mock-popout-button", + template: ` + + `, + standalone: true, + imports: [IconButtonModule], +}) +class MockPopoutButtonComponent {} + +@Component({ + selector: "mock-current-account", + template: ` + + `, + standalone: true, + imports: [AvatarModule], +}) +class MockCurrentAccountComponent {} + +@Component({ + selector: "mock-vault-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPageComponent {} + +@Component({ + selector: "mock-vault-page-popped", + template: ` + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultPagePoppedComponent {} + +@Component({ + selector: "mock-generator-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + GeneratorComponent, + ], +}) +class MockGeneratorPageComponent {} + +@Component({ + selector: "mock-send-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SendComponent, + ], +}) +class MockSendPageComponent {} + +@Component({ + selector: "mock-settings-page", + template: ` + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + SettingsComponent, + ], +}) +class MockSettingsPageComponent {} + +@Component({ + selector: "mock-vault-subpage", + template: ` + + + + + + + + + + + + + `, + standalone: true, + imports: [ + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + ButtonModule, + MockAddButtonComponent, + MockPopoutButtonComponent, + MockCurrentAccountComponent, + VaultComponent, + ], +}) +class MockVaultSubpageComponent {} + +export default { + title: "Browser/Popup Layout", + component: PopupPageComponent, + decorators: [ + moduleMetadata({ + imports: [ + PopupTabNavigationComponent, + CommonModule, + RouterModule, + ExtensionContainerComponent, + MockVaultSubpageComponent, + MockVaultPageComponent, + MockSendPageComponent, + MockGeneratorPageComponent, + MockSettingsPageComponent, + MockVaultPagePoppedComponent, + ], + providers: [ + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + back: "Back", + }); + }, + }, + ], + }), + applicationConfig({ + providers: [ + importProvidersFrom( + RouterModule.forRoot( + [ + { path: "", redirectTo: "tabs/vault", pathMatch: "full" }, + { path: "tabs/vault", component: MockVaultPageComponent }, + { path: "tabs/generator", component: MockGeneratorPageComponent }, + { path: "tabs/send", component: MockSendPageComponent }, + { path: "tabs/settings", component: MockSettingsPageComponent }, + // in case you are coming from a story that also uses the router + { path: "**", redirectTo: "tabs/vault" }, + ], + { useHash: true }, + ), + ), + ], + }), + ], +} as Meta; + +type Story = StoryObj; + +export const PopupTabNavigation: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + + + `, + }), +}; + +export const PopupPage: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PopupPageWithFooter: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` + + + + `, + }), +}; + +export const PoppedOut: Story = { + render: (args) => ({ + props: args, + template: /* HTML */ ` +
+ +
+ `, + }), +}; diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.html b/apps/browser/src/platform/popup/layout/popup-page.component.html new file mode 100644 index 0000000000..ba871d6319 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.html @@ -0,0 +1,7 @@ + +
+
+ +
+
+ diff --git a/apps/browser/src/platform/popup/layout/popup-page.component.ts b/apps/browser/src/platform/popup/layout/popup-page.component.ts new file mode 100644 index 0000000000..1223a6f418 --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-page.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "popup-page", + templateUrl: "popup-page.component.html", + standalone: true, + host: { + class: "tw-h-full tw-flex tw-flex-col tw-flex-1 tw-overflow-y-auto", + }, +}) +export class PopupPageComponent {} diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html new file mode 100644 index 0000000000..a0ff252c6c --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.html @@ -0,0 +1,32 @@ +
+ +
+ diff --git a/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts new file mode 100644 index 0000000000..ced3f6462e --- /dev/null +++ b/apps/browser/src/platform/popup/layout/popup-tab-navigation.component.ts @@ -0,0 +1,43 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { RouterModule } from "@angular/router"; + +import { LinkModule } from "@bitwarden/components"; + +@Component({ + selector: "popup-tab-navigation", + templateUrl: "popup-tab-navigation.component.html", + standalone: true, + imports: [CommonModule, LinkModule, RouterModule], + host: { + class: "tw-block tw-h-full tw-w-full tw-flex tw-flex-col", + }, +}) +export class PopupTabNavigationComponent { + navButtons = [ + { + label: "Vault", + page: "/tabs/vault", + iconKey: "lock", + iconKeyActive: "lock-f", + }, + { + label: "Generator", + page: "/tabs/generator", + iconKey: "generate", + iconKeyActive: "generate-f", + }, + { + label: "Send", + page: "/tabs/send", + iconKey: "send", + iconKeyActive: "send-f", + }, + { + label: "Settings", + page: "/tabs/settings", + iconKey: "cog", + iconKeyActive: "cog-f", + }, + ]; +} diff --git a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts index 64935ab591..259d6f154a 100644 --- a/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts +++ b/apps/browser/src/platform/services/abstractions/abstract-chrome-storage-api.service.ts @@ -78,6 +78,11 @@ export default abstract class AbstractChromeStorageService async save(key: string, obj: any): Promise { obj = objToStore(obj); + if (obj == null) { + // Safari does not support set of null values + return this.remove(key); + } + const keyedObj = { [key]: obj }; return new Promise((resolve) => { this.chromeStorageApi.set(keyedObj, () => { diff --git a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts index 812901879d..ceadc16a58 100644 --- a/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts +++ b/apps/browser/src/platform/services/abstractions/chrome-storage-api.service.spec.ts @@ -62,6 +62,17 @@ describe("ChromeStorageApiService", () => { expect.any(Function), ); }); + + it("removes the key when the value is null", async () => { + const removeMock = chrome.storage.local.remove as jest.Mock; + removeMock.mockImplementation((key, callback) => { + delete store[key]; + callback(); + }); + const key = "key"; + await service.save(key, null); + expect(removeMock).toHaveBeenCalledWith(key, expect.any(Function)); + }); }); describe("get", () => { diff --git a/apps/browser/src/platform/services/abstractions/script-injector.service.ts b/apps/browser/src/platform/services/abstractions/script-injector.service.ts new file mode 100644 index 0000000000..b41e5c7617 --- /dev/null +++ b/apps/browser/src/platform/services/abstractions/script-injector.service.ts @@ -0,0 +1,45 @@ +export type CommonScriptInjectionDetails = { + /** + * Script injected into the document. + * Overridden by `mv2Details` and `mv3Details`. + */ + file?: string; + /** + * Identifies the frame targeted for script injection. Defaults to the top level frame (0). + * Can also be set to "all_frames" to inject into all frames in a tab. + */ + frame?: "all_frames" | number; + /** + * When the script executes. Defaults to "document_start". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/content_scripts + */ + runAt?: "document_start" | "document_end" | "document_idle"; +}; + +export type Mv2ScriptInjectionDetails = { + file: string; +}; + +export type Mv3ScriptInjectionDetails = { + file: string; + /** + * The world in which the script should be executed. Defaults to "ISOLATED". + * @see https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/scripting/ExecutionWorld + */ + world?: chrome.scripting.ExecutionWorld; +}; + +/** + * Configuration for injecting a script into a tab. The `file` property should present as a + * path that is relative to the root directory of the extension build, ie "content/script.js". + */ +export type ScriptInjectionConfig = { + tabId: number; + injectDetails: CommonScriptInjectionDetails; + mv2Details?: Mv2ScriptInjectionDetails; + mv3Details?: Mv3ScriptInjectionDetails; +}; + +export abstract class ScriptInjectorService { + abstract inject(config: ScriptInjectionConfig): Promise; +} diff --git a/apps/browser/src/platform/services/browser-crypto.service.ts b/apps/browser/src/platform/services/browser-crypto.service.ts index d7533a22d6..cd23c916c6 100644 --- a/apps/browser/src/platform/services/browser-crypto.service.ts +++ b/apps/browser/src/platform/services/browser-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -28,6 +29,7 @@ export class BrowserCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -39,6 +41,7 @@ export class BrowserCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } override async hasUserKeyStored(keySuffix: KeySuffixOptions, userId?: UserId): Promise { diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts deleted file mode 100644 index 0c7008473b..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-background.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModeBackgroundService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenPopupMainMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts b/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts deleted file mode 100644 index 5883f61197..0000000000 --- a/apps/browser/src/platform/services/browser-messaging-private-mode-popup.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export default class BrowserMessagingPrivateModePopupService implements MessagingService { - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - (self as any).bitwardenBackgroundMessageListener(message); - } -} diff --git a/apps/browser/src/platform/services/browser-messaging.service.ts b/apps/browser/src/platform/services/browser-messaging.service.ts deleted file mode 100644 index 5eff957cb5..0000000000 --- a/apps/browser/src/platform/services/browser-messaging.service.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -import { BrowserApi } from "../browser/browser-api"; - -export default class BrowserMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - return BrowserApi.sendMessage(subscriber, arg); - } -} diff --git a/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts new file mode 100644 index 0000000000..db5b3df7a3 --- /dev/null +++ b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.spec.ts @@ -0,0 +1,97 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptionType } from "@bitwarden/common/platform/enums"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { InitializerKey } from "@bitwarden/common/platform/services/cryptography/initializer-key"; +import { makeStaticByteArray } from "@bitwarden/common/spec"; + +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +import { BrowserMultithreadEncryptServiceImplementation } from "./browser-multithread-encrypt.service.implementation"; + +describe("BrowserMultithreadEncryptServiceImplementation", () => { + let cryptoFunctionServiceMock: MockProxy; + let logServiceMock: MockProxy; + let offscreenDocumentServiceMock: MockProxy; + let encryptService: BrowserMultithreadEncryptServiceImplementation; + const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + const sendMessageWithResponseSpy = jest.spyOn(BrowserApi, "sendMessageWithResponse"); + const encType = EncryptionType.AesCbc256_HmacSha256_B64; + const key = new SymmetricCryptoKey(makeStaticByteArray(64, 100), encType); + const items: Decryptable[] = [ + { + decrypt: jest.fn(), + initializerKey: InitializerKey.Cipher, + }, + ]; + + beforeEach(() => { + cryptoFunctionServiceMock = mock(); + logServiceMock = mock(); + offscreenDocumentServiceMock = mock({ + withDocument: jest.fn((_, __, callback) => callback() as any), + }); + encryptService = new BrowserMultithreadEncryptServiceImplementation( + cryptoFunctionServiceMock, + logServiceMock, + false, + offscreenDocumentServiceMock, + ); + manifestVersionSpy.mockReturnValue(3); + sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify([])); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("decrypts items using web workers if the chrome.offscreen API is not supported", async () => { + manifestVersionSpy.mockReturnValue(2); + + await encryptService.decryptItems([], key); + + expect(offscreenDocumentServiceMock.withDocument).not.toHaveBeenCalled(); + }); + + it("decrypts items using the chrome.offscreen API if it is supported", async () => { + sendMessageWithResponseSpy.mockResolvedValue(JSON.stringify(items)); + + await encryptService.decryptItems(items, key); + + expect(offscreenDocumentServiceMock.withDocument).toHaveBeenCalledWith( + [chrome.offscreen.Reason.WORKERS], + "Use web worker to decrypt items.", + expect.any(Function), + ); + expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenDecryptItems", { + decryptRequest: expect.any(String), + }); + }); + + it("returns an empty array if the passed items are not defined", async () => { + const result = await encryptService.decryptItems(null, key); + + expect(result).toEqual([]); + }); + + it("returns an empty array if the offscreen document message returns an empty value", async () => { + sendMessageWithResponseSpy.mockResolvedValue(""); + + const result = await encryptService.decryptItems(items, key); + + expect(result).toEqual([]); + }); + + it("returns an empty array if the offscreen document message returns an empty array", async () => { + sendMessageWithResponseSpy.mockResolvedValue("[]"); + + const result = await encryptService.decryptItems(items, key); + + expect(result).toEqual([]); + }); +}); diff --git a/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts new file mode 100644 index 0000000000..ace5015c8e --- /dev/null +++ b/apps/browser/src/platform/services/browser-multithread-encrypt.service.implementation.ts @@ -0,0 +1,91 @@ +import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { Decryptable } from "@bitwarden/common/platform/interfaces/decryptable.interface"; +import { InitializerMetadata } from "@bitwarden/common/platform/interfaces/initializer-metadata.interface"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; + +import { BrowserApi } from "../browser/browser-api"; +import { OffscreenDocumentService } from "../offscreen-document/abstractions/offscreen-document"; + +export class BrowserMultithreadEncryptServiceImplementation extends MultithreadEncryptServiceImplementation { + constructor( + cryptoFunctionService: CryptoFunctionService, + logService: LogService, + logMacFailures: boolean, + private offscreenDocumentService: OffscreenDocumentService, + ) { + super(cryptoFunctionService, logService, logMacFailures); + } + + /** + * Handles decryption of items, will use the offscreen document if supported. + * + * @param items - The items to decrypt. + * @param key - The key to use for decryption. + */ + async decryptItems( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (!this.isOffscreenDocumentSupported()) { + return await super.decryptItems(items, key); + } + + return await this.decryptItemsInOffscreenDocument(items, key); + } + + /** + * Decrypts items using the offscreen document api. + * + * @param items - The items to decrypt. + * @param key - The key to use for decryption. + */ + private async decryptItemsInOffscreenDocument( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { + if (items == null || items.length < 1) { + return []; + } + + const request = { + id: Utils.newGuid(), + items: items, + key: key, + }; + + const response = await this.offscreenDocumentService.withDocument( + [chrome.offscreen.Reason.WORKERS], + "Use web worker to decrypt items.", + async () => { + return (await BrowserApi.sendMessageWithResponse("offscreenDecryptItems", { + decryptRequest: JSON.stringify(request), + })) as string; + }, + ); + + if (!response) { + return []; + } + + const responseItems = JSON.parse(response); + if (responseItems?.length < 1) { + return []; + } + + return this.initializeItems(responseItems); + } + + /** + * Checks if the offscreen document api is supported. + */ + private isOffscreenDocumentSupported() { + return ( + BrowserApi.isManifestVersion(3) && + typeof chrome !== "undefined" && + typeof chrome.offscreen !== "undefined" + ); + } +} 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 new file mode 100644 index 0000000000..d6ec3dfde9 --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.spec.ts @@ -0,0 +1,180 @@ +import { mock } from "jest-mock-extended"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + Mv3ScriptInjectionDetails, +} from "./abstractions/script-injector.service"; +import { BrowserScriptInjectorService } from "./browser-script-injector.service"; + +describe("ScriptInjectorService", () => { + const tabId = 1; + const combinedManifestVersionFile = "content/autofill-init.js"; + const mv2SpecificFile = "content/autofill-init-mv2.js"; + const mv2Details = { file: mv2SpecificFile }; + const mv3SpecificFile = "content/autofill-init-mv3.js"; + const mv3Details: Mv3ScriptInjectionDetails = { file: mv3SpecificFile, world: "MAIN" }; + const sharedInjectDetails: CommonScriptInjectionDetails = { + runAt: "document_start", + }; + const manifestVersionSpy = jest.spyOn(BrowserApi, "manifestVersion", "get"); + let scriptInjectorService: BrowserScriptInjectorService; + jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); + jest.spyOn(BrowserApi, "isManifestVersion"); + const platformUtilsService = mock(); + const logService = mock(); + + beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); + }); + + describe("inject", () => { + describe("injection of a single script that functions in both manifest v2 and v3", () => { + it("injects the script in manifest v2 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: "all_frames", + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + allFrames: true, + file: combinedManifestVersionFile, + }); + }); + + it("injects the script in manifest v3 when given combined injection details", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + tabId, + injectDetails: { + file: combinedManifestVersionFile, + frame: 10, + ...sharedInjectDetails, + }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 10, file: combinedManifestVersionFile }, + { world: "ISOLATED" }, + ); + }); + }); + + describe("injection of mv2 specific details", () => { + describe("given the extension is running manifest v2", () => { + it("injects the mv2 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: mv2SpecificFile, + }); + }); + }); + + describe("given the extension is running manifest v3", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }, + { world: "ISOLATED" }, + ); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(3); + + await expect( + scriptInjectorService.inject({ + mv2Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: null }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + + describe("injection of mv3 specific details", () => { + describe("given the extension is running manifest v3", () => { + it("injects the mv3 script injection details file", async () => { + manifestVersionSpy.mockReturnValue(3); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: sharedInjectDetails, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith( + tabId, + { ...sharedInjectDetails, frameId: 0, file: mv3SpecificFile }, + { world: "MAIN" }, + ); + }); + }); + + describe("given the extension is running manifest v2", () => { + it("injects the common script injection details file", async () => { + manifestVersionSpy.mockReturnValue(2); + + await scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: combinedManifestVersionFile }, + }); + + expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabId, { + ...sharedInjectDetails, + frameId: 0, + file: combinedManifestVersionFile, + }); + }); + + it("throws an error if no common script injection details file is specified", async () => { + manifestVersionSpy.mockReturnValue(2); + + await expect( + scriptInjectorService.inject({ + mv3Details, + tabId, + injectDetails: { ...sharedInjectDetails, file: "" }, + }), + ).rejects.toThrow("No file specified for script injection"); + }); + }); + }); + }); +}); diff --git a/apps/browser/src/platform/services/browser-script-injector.service.ts b/apps/browser/src/platform/services/browser-script-injector.service.ts new file mode 100644 index 0000000000..5b3a10ef2b --- /dev/null +++ b/apps/browser/src/platform/services/browser-script-injector.service.ts @@ -0,0 +1,105 @@ +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { BrowserApi } from "../browser/browser-api"; + +import { + CommonScriptInjectionDetails, + ScriptInjectionConfig, + ScriptInjectorService, +} from "./abstractions/script-injector.service"; + +export class BrowserScriptInjectorService extends ScriptInjectorService { + constructor( + private readonly platformUtilsService: PlatformUtilsService, + private readonly logService: LogService, + ) { + super(); + } + + /** + * Facilitates the injection of a script into a tab context. Will adjust + * behavior between manifest v2 and v3 based on the passed configuration. + * + * @param config - The configuration for the script injection. + */ + async inject(config: ScriptInjectionConfig): Promise { + const { tabId, injectDetails, mv3Details } = config; + const file = this.getScriptFile(config); + if (!file) { + throw new Error("No file specified for script injection"); + } + + const injectionDetails = this.buildInjectionDetails(injectDetails, file); + + if (BrowserApi.isManifestVersion(3)) { + try { + await BrowserApi.executeScriptInTab(tabId, injectionDetails, { + world: mv3Details?.world ?? "ISOLATED", + }); + } catch (error) { + // Swallow errors for host permissions, since this is believed to be a Manifest V3 Chrome bug + // @TODO remove when the bugged behaviour is resolved + if ( + error.message !== + "Cannot access contents of the page. Extension manifest must request permission to access the respective host." + ) { + throw error; + } + + if (this.platformUtilsService.isDev()) { + this.logService.warning( + `BrowserApi.executeScriptInTab exception for ${injectDetails.file} in tab ${tabId}: ${error.message}`, + ); + } + } + + return; + } + + await BrowserApi.executeScriptInTab(tabId, injectionDetails); + } + + /** + * Retrieves the script file to inject based on the configuration. + * + * @param config - The configuration for the script injection. + */ + private getScriptFile(config: ScriptInjectionConfig): string { + const { injectDetails, mv2Details, mv3Details } = config; + + if (BrowserApi.isManifestVersion(3)) { + return mv3Details?.file ?? injectDetails?.file; + } + + return mv2Details?.file ?? injectDetails?.file; + } + + /** + * Builds the injection details for the script injection. + * + * @param injectDetails - The details for the script injection. + * @param file - The file to inject. + */ + private buildInjectionDetails( + injectDetails: CommonScriptInjectionDetails, + file: string, + ): chrome.tabs.InjectDetails { + const { frame, runAt } = injectDetails; + const injectionDetails: chrome.tabs.InjectDetails = { file }; + + if (runAt) { + injectionDetails.runAt = runAt; + } + + if (!frame) { + return { ...injectionDetails, frameId: 0 }; + } + + if (frame !== "all_frames") { + return { ...injectionDetails, frameId: frame }; + } + + return { ...injectionDetails, allFrames: true }; + } +} diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts index 8f43998321..506f185b64 100644 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ b/apps/browser/src/platform/services/browser-state.service.spec.ts @@ -1,13 +1,9 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom } from "rxjs"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { State } from "@bitwarden/common/platform/models/domain/state"; @@ -19,15 +15,11 @@ import { Account } from "../../models/account"; import { DefaultBrowserStateService } from "./default-browser-state.service"; -// disable session syncing to just test class -jest.mock("../decorators/session-sync-observable/"); - describe("Browser State Service", () => { let secureStorageService: MockProxy; let diskStorageService: MockProxy; let logService: MockProxy; let stateFactory: MockProxy>; - let useAccountCache: boolean; let environmentService: MockProxy; let tokenService: MockProxy; let migrationRunner: MockProxy; @@ -46,14 +38,11 @@ describe("Browser State Service", () => { environmentService = mock(); tokenService = mock(); migrationRunner = mock(); - // turn off account cache for tests - useAccountCache = false; state = new State(new GlobalState()); state.accounts[userId] = new Account({ profile: { userId: userId }, }); - state.activeUserId = userId; }); afterEach(() => { @@ -61,7 +50,7 @@ describe("Browser State Service", () => { }); describe("state methods", () => { - let memoryStorageService: MockProxy; + let memoryStorageService: MockProxy; beforeEach(() => { memoryStorageService = mock(); @@ -78,22 +67,11 @@ describe("Browser State Service", () => { environmentService, tokenService, migrationRunner, - useAccountCache, ); }); - describe("add Account", () => { - it("should add account", async () => { - const newUserId = "newUserId" as UserId; - const newAcct = new Account({ - profile: { userId: newUserId }, - }); - - await sut.addAccount(newAcct); - - const accts = await firstValueFrom(sut.accounts$); - expect(accts[newUserId]).toBeDefined(); - }); + it("exists", () => { + expect(sut).toBeDefined(); }); }); }); diff --git a/apps/browser/src/platform/services/default-browser-state.service.ts b/apps/browser/src/platform/services/default-browser-state.service.ts index f1f306dbc0..92da28efa2 100644 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ b/apps/browser/src/platform/services/default-browser-state.service.ts @@ -1,13 +1,8 @@ -import { BehaviorSubject } from "rxjs"; - import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractStorageService, - AbstractMemoryStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; @@ -15,37 +10,25 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; import { Account } from "../../models/account"; -import { BrowserApi } from "../browser/browser-api"; -import { browserSession, sessionSync } from "../decorators/session-sync-observable"; import { BrowserStateService } from "./abstractions/browser-state.service"; -@browserSession export class DefaultBrowserStateService extends BaseStateService implements BrowserStateService { - @sessionSync({ - initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account - initializeAs: "record", - }) - protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>; - @sessionSync({ initializer: (s: string) => s }) - protected activeAccountSubject: BehaviorSubject; - protected accountDeserializer = Account.fromJSON; constructor( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, stateFactory: StateFactory, accountService: AccountService, environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - useAccountCache = true, ) { super( storageService, @@ -57,45 +40,7 @@ export class DefaultBrowserStateService environmentService, tokenService, migrationRunner, - useAccountCache, ); - - // TODO: This is a hack to fix having a disk cache on both the popup and - // the background page that can get out of sync. We need to work out the - // best way to handle caching with multiple instances of the state service. - if (useAccountCache) { - BrowserApi.storageChangeListener((changes, namespace) => { - if (namespace === "local") { - for (const key of Object.keys(changes)) { - if (key !== "accountActivity" && this.accountDiskCache.value[key]) { - this.deleteDiskCache(key); - } - } - } - }); - - BrowserApi.addListener( - chrome.runtime.onMessage, - (message: { command: string }, _, respond) => { - if (message.command === "initializeDiskCache") { - respond(JSON.stringify(this.accountDiskCache.value)); - } - }, - ); - } - } - - override async initAccountState(): Promise { - if (this.isRecoveredSession && this.useAccountCache) { - // request cache initialization - - const response = await BrowserApi.sendMessageWithResponse("initializeDiskCache"); - this.accountDiskCache.next(JSON.parse(response)); - - return; - } - - await super.initAccountState(); } async addAccount(account: Account) { @@ -104,15 +49,6 @@ export class DefaultBrowserStateService await super.addAccount(account); } - async getIsAuthenticated(options?: StorageOptions): Promise { - // Firefox Private Mode can clash with non-Private Mode because they both read from the same onDiskOptions - // Check that there is an account in memory before considering the user authenticated - return ( - (await super.getIsAuthenticated(options)) && - (await this.getAccount(await this.defaultInMemoryOptions())) != null - ); - } - // Overriding the base class to prevent deleting the cache on save. We register a storage listener // to delete the cache in the constructor above. protected override async saveAccountToDisk( 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 7740a22071..8d43c8f2fe 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 @@ -1,345 +1,178 @@ import { mock, MockProxy } from "jest-mock-extended"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; -import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, - StorageUpdate, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; 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 { FakeStorageService, makeEncString } from "@bitwarden/common/spec"; import { LocalBackedSessionStorageService } from "./local-backed-session-storage.service"; describe("LocalBackedSessionStorage", () => { + const sessionKey = new SymmetricCryptoKey( + Utils.fromUtf8ToArray("00000000000000000000000000000000"), + ); + let localStorage: FakeStorageService; let encryptService: MockProxy; - let keyGenerationService: MockProxy; - let localStorageService: MockProxy; - let sessionStorageService: MockProxy; - - let cache: Map; - const testObj = { a: 1, b: 2 }; - - const key = new SymmetricCryptoKey(Utils.fromUtf8ToArray("00000000000000000000000000000000")); - let getSessionKeySpy: jest.SpyInstance; - let sendUpdateSpy: jest.SpyInstance; - const mockEnc = (input: string) => Promise.resolve(new EncString("ENCRYPTED" + input)); + let platformUtilsService: MockProxy; + let logService: MockProxy; let sut: LocalBackedSessionStorageService; - const mockExistingSessionKey = (key: SymmetricCryptoKey) => { - sessionStorageService.get.mockImplementation((storageKey) => { - if (storageKey === "localEncryptionKey_test") { - return Promise.resolve(key?.toJSON()); - } - - return Promise.reject("No implementation for " + storageKey); - }); - }; - beforeEach(() => { + localStorage = new FakeStorageService(); encryptService = mock(); - keyGenerationService = mock(); - localStorageService = mock(); - sessionStorageService = mock(); + platformUtilsService = mock(); + logService = mock(); sut = new LocalBackedSessionStorageService( + new Lazy(async () => sessionKey), + localStorage, encryptService, - keyGenerationService, - localStorageService, - sessionStorageService, - "test", + platformUtilsService, + logService, ); - - cache = sut["cache"]; - - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - derivedKey: key, - salt: "bitwarden-ephemeral", - material: null, // Not used - }); - - getSessionKeySpy = jest.spyOn(sut, "getSessionEncKey"); - getSessionKeySpy.mockResolvedValue(key); - - sendUpdateSpy = jest.spyOn(sut, "sendUpdate"); - sendUpdateSpy.mockReturnValue(); }); describe("get", () => { - it("should return from cache", async () => { - cache.set("test", testObj); + it("return the cached value when one is cached", async () => { + sut["cache"]["test"] = "cached"; const result = await sut.get("test"); - expect(result).toStrictEqual(testObj); + expect(result).toEqual("cached"); }); - describe("not in cache", () => { - const session = { test: testObj }; + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.get("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - beforeEach(() => { - mockExistingSessionKey(key); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.get("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); + }); - describe("no session retrieved", () => { - let result: any; - let spy: jest.SpyInstance; - beforeEach(async () => { - spy = jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - localStorageService.get.mockResolvedValue(null); - result = await sut.get("test"); - }); + it("returns a decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.get("test"); + expect(encryptService.decryptToUtf8).toHaveBeenCalledWith(encrypted, sessionKey); + expect(result).toEqual("decrypted"); + }); - it("should grab from session if not in cache", async () => { - expect(spy).toHaveBeenCalledWith(key); - }); - - it("should return null if session is null", async () => { - expect(result).toBeNull(); - }); - }); - - describe("session retrieved from storage", () => { - beforeEach(() => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(session); - }); - - it("should return null if session does not have the key", async () => { - const result = await sut.get("DNE"); - expect(result).toBeNull(); - }); - - it("should return the value retrieved from session", async () => { - const result = await sut.get("test"); - expect(result).toEqual(session.test); - }); - - it("should set retrieved values in cache", async () => { - await sut.get("test"); - expect(cache.has("test")).toBe(true); - expect(cache.get("test")).toEqual(session.test); - }); - - it("should use a deserializer if provided", async () => { - const deserializer = jest.fn().mockReturnValue(testObj); - const result = await sut.get("test", { deserializer: deserializer }); - expect(deserializer).toHaveBeenCalledWith(session.test); - expect(result).toEqual(testObj); - }); - }); + it("caches the decrypted value when one is stored in local storage", async () => { + const encrypted = makeEncString("encrypted"); + localStorage.internalStore["session_test"] = encrypted.encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + await sut.get("test"); + expect(sut["cache"]["test"]).toEqual("decrypted"); }); }); describe("has", () => { - it("should be false if `get` returns null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue(null); - expect(await sut.has("test")).toBe(false); + it("returns false when the key is not in cache", async () => { + const result = await sut.has("test"); + expect(result).toBe(false); + }); + + it("returns true when the key is in cache", async () => { + sut["cache"]["test"] = "cached"; + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it("returns true when the key is in local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + encryptService.decryptToUtf8.mockResolvedValue(JSON.stringify("decrypted")); + const result = await sut.has("test"); + expect(result).toBe(true); + }); + + it.each([null, undefined])("returns false when %s is cached", async (nullish) => { + sut["cache"]["test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + }); + + it.each([null, undefined])( + "returns false when null is stored in local storage", + async (nullish) => { + localStorage.internalStore["session_test"] = nullish; + await expect(sut.has("test")).resolves.toBe(false); + expect(encryptService.decryptToUtf8).not.toHaveBeenCalled(); + }, + ); + }); + + describe("save", () => { + const encString = makeEncString("encrypted"); + beforeEach(() => { + encryptService.encrypt.mockResolvedValue(encString); + }); + + it("logs a warning when saving the same value twice and in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(true); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).toHaveBeenCalled(); + }); + + it("does not log when saving the same value twice and not in a dev environment", async () => { + platformUtilsService.isDev.mockReturnValue(false); + sut["cache"]["test"] = "cached"; + await sut.save("test", "cached"); + expect(logService.warning).not.toHaveBeenCalled(); + }); + + it("removes the key when saving a null value", async () => { + const spy = jest.spyOn(sut, "remove"); + await sut.save("test", null); expect(spy).toHaveBeenCalledWith("test"); }); - it("should be true if `get` returns non-null", async () => { - const spy = jest.spyOn(sut, "get"); - spy.mockResolvedValue({}); - expect(await sut.has("test")).toBe(true); - expect(spy).toHaveBeenCalledWith("test"); + it("saves the value to cache", async () => { + await sut.save("test", "value"); + expect(sut["cache"]["test"]).toEqual("value"); + }); + + it("encrypts and saves the value to local storage", async () => { + await sut.save("test", "value"); + expect(encryptService.encrypt).toHaveBeenCalledWith(JSON.stringify("value"), sessionKey); + expect(localStorage.internalStore["session_test"]).toEqual(encString.encryptedString); + }); + + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.save("test", "value"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "save" }); }); }); describe("remove", () => { - it("should save null", async () => { + it("nulls the value in cache", async () => { + sut["cache"]["test"] = "cached"; await sut.remove("test"); - expect(sendUpdateSpy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); - }); - }); - - describe("save", () => { - describe("caching", () => { - beforeEach(() => { - localStorageService.get.mockResolvedValue(null); - sessionStorageService.get.mockResolvedValue(null); - - localStorageService.save.mockResolvedValue(); - sessionStorageService.save.mockResolvedValue(); - - encryptService.encrypt.mockResolvedValue(mockEnc("{}")); - }); - - it("should remove key from cache if value is null", async () => { - cache.set("test", {}); - const cacheSetSpy = jest.spyOn(cache, "set"); - expect(cache.has("test")).toBe(true); - await sut.save("test", null); - // Don't remove from cache, just replace with null - expect(cache.get("test")).toBe(null); - expect(cacheSetSpy).toHaveBeenCalledWith("test", null); - }); - - it("should set cache if value is non-null", async () => { - expect(cache.has("test")).toBe(false); - const setSpy = jest.spyOn(cache, "set"); - await sut.save("test", testObj); - expect(cache.get("test")).toBe(testObj); - expect(setSpy).toHaveBeenCalledWith("test", testObj); - }); + expect(sut["cache"]["test"]).toBeNull(); }); - describe("local storing", () => { - let setSpy: jest.SpyInstance; - - beforeEach(() => { - setSpy = jest.spyOn(sut, "setLocalSession").mockResolvedValue(); - }); - - it("should store a new session", async () => { - jest.spyOn(sut, "getLocalSession").mockResolvedValue(null); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - - it("should update an existing session", async () => { - const existingObj = { test: testObj }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test2", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test2: testObj, ...existingObj }, key); - }); - - it("should overwrite an existing item in session", async () => { - const existingObj = { test: {} }; - jest.spyOn(sut, "getLocalSession").mockResolvedValue(existingObj); - await sut.save("test", testObj); - - expect(setSpy).toHaveBeenCalledWith({ test: testObj }, key); - }); - }); - }); - - describe("getSessionKey", () => { - beforeEach(() => { - getSessionKeySpy.mockRestore(); + it("removes the key from local storage", async () => { + localStorage.internalStore["session_test"] = makeEncString("encrypted").encryptedString; + await sut.remove("test"); + expect(localStorage.internalStore["session_test"]).toBeUndefined(); }); - it("should return the stored symmetric crypto key", async () => { - sessionStorageService.get.mockResolvedValue({ ...key }); - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - }); - - describe("new key creation", () => { - beforeEach(() => { - keyGenerationService.createKeyWithPurpose.mockResolvedValue({ - salt: "salt", - material: null, - derivedKey: key, - }); - jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - }); - - it("should create a symmetric crypto key", async () => { - const result = await sut.getSessionEncKey(); - - expect(result).toStrictEqual(key); - expect(keyGenerationService.createKeyWithPurpose).toHaveBeenCalledTimes(1); - }); - - it("should store a symmetric crypto key if it makes one", async () => { - const spy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - await sut.getSessionEncKey(); - - expect(spy).toHaveBeenCalledWith(key); - }); - }); - }); - - describe("getLocalSession", () => { - it("should return null if session is null", async () => { - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(localStorageService.get).toHaveBeenCalledWith("session_test"); - }); - - describe("non-null sessions", () => { - const session = { test: "test" }; - const encSession = new EncString(JSON.stringify(session)); - const decryptedSession = JSON.stringify(session); - - beforeEach(() => { - localStorageService.get.mockResolvedValue(encSession.encryptedString); - }); - - it("should decrypt returned sessions", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - await sut.getLocalSession(key); - expect(encryptService.decryptToUtf8).toHaveBeenNthCalledWith(1, encSession, key); - }); - - it("should parse session", async () => { - encryptService.decryptToUtf8 - .calledWith(expect.anything(), key) - .mockResolvedValue(decryptedSession); - const result = await sut.getLocalSession(key); - expect(result).toEqual(session); - }); - - it("should remove state if decryption fails", async () => { - encryptService.decryptToUtf8.mockResolvedValue(null); - const setSessionEncKeySpy = jest.spyOn(sut, "setSessionEncKey").mockResolvedValue(); - - const result = await sut.getLocalSession(key); - - expect(result).toBeNull(); - expect(setSessionEncKeySpy).toHaveBeenCalledWith(null); - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - }); - }); - - describe("setLocalSession", () => { - const testSession = { test: "a" }; - const testJSON = JSON.stringify(testSession); - - it("should encrypt a stringified session", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - localStorageService.save.mockResolvedValue(); - await sut.setLocalSession(testSession, key); - - expect(encryptService.encrypt).toHaveBeenNthCalledWith(1, testJSON, key); - }); - - it("should remove local session if null", async () => { - encryptService.encrypt.mockResolvedValue(null); - await sut.setLocalSession(null, key); - - expect(localStorageService.remove).toHaveBeenCalledWith("session_test"); - }); - - it("should save encrypted string", async () => { - encryptService.encrypt.mockImplementation(mockEnc); - await sut.setLocalSession(testSession, key); - - expect(localStorageService.save).toHaveBeenCalledWith( - "session_test", - (await mockEnc(testJSON)).encryptedString, - ); - }); - }); - - describe("setSessionKey", () => { - it("should remove if null", async () => { - await sut.setSessionEncKey(null); - expect(sessionStorageService.remove).toHaveBeenCalledWith("localEncryptionKey_test"); - }); - - it("should save key when not null", async () => { - await sut.setSessionEncKey(key); - expect(sessionStorageService.save).toHaveBeenCalledWith("localEncryptionKey_test", key); + it("emits an update", async () => { + const spy = jest.spyOn(sut["updatesSubject"], "next"); + await sut.remove("test"); + expect(spy).toHaveBeenCalledWith({ key: "test", updateType: "remove" }); }); }); }); 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 3f01e4169e..2c14ac2833 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 @@ -1,87 +1,77 @@ -import { Observable, Subject, filter, map, merge, share, tap } from "rxjs"; -import { Jsonify } from "type-fest"; +import { Subject } from "rxjs"; 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 { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { MemoryStorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; +import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { fromChromeEvent } from "../browser/from-chrome-event"; -import { devFlag } from "../decorators/dev-flag.decorator"; -import { devFlagEnabled } from "../flags"; +import { BrowserApi } from "../browser/browser-api"; +import { MemoryStoragePortMessage } from "../storage/port-messages"; +import { portName } from "../storage/port-name"; export class LocalBackedSessionStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { - private cache = new Map(); + private ports: Set = new Set([]); + private cache: Record = {}; private updatesSubject = new Subject(); - - private commandName = `localBackedSessionStorage_${this.name}`; - private encKey = `localEncryptionKey_${this.name}`; - private sessionKey = `session_${this.name}`; - - updates$: Observable; + readonly valuesRequireDeserialization = true; + updates$ = this.updatesSubject.asObservable(); constructor( - private encryptService: EncryptService, - private keyGenerationService: KeyGenerationService, - private localStorage: AbstractStorageService, - private sessionStorage: AbstractStorageService, - private name: string, + private readonly sessionKey: Lazy>, + private readonly localStorage: AbstractStorageService, + private readonly encryptService: EncryptService, + private readonly platformUtilsService: PlatformUtilsService, + private readonly logService: LogService, ) { super(); - const remoteObservable = fromChromeEvent(chrome.runtime.onMessage).pipe( - filter(([msg]) => msg.command === this.commandName), - map(([msg]) => msg.update as StorageUpdate), - tap((update) => { - if (update.updateType === "remove") { - this.cache.set(update.key, null); - } else { - this.cache.delete(update.key); - } - }), - share(), - ); + BrowserApi.addListener(chrome.runtime.onConnect, (port) => { + if (port.name !== portName(chrome.storage.session)) { + return; + } - remoteObservable.subscribe(); + this.ports.add(port); - this.updates$ = merge(this.updatesSubject.asObservable(), remoteObservable); + const listenerCallback = this.onMessageFromForeground.bind(this); + port.onDisconnect.addListener(() => { + this.ports.delete(port); + port.onMessage.removeListener(listenerCallback); + }); + port.onMessage.addListener(listenerCallback); + // Initialize the new memory storage service with existing data + this.sendMessageTo(port, { + action: "initialization", + data: Array.from(Object.keys(this.cache)), + }); + this.updates$.subscribe((update) => { + this.broadcastMessage({ + action: "subject_update", + data: update, + }); + }); + }); } - get valuesRequireDeserialization(): boolean { - return true; - } - - async get(key: string, options?: MemoryStorageOptions): Promise { - if (this.cache.has(key)) { - return this.cache.get(key) as T; + async get(key: string, options?: StorageOptions): Promise { + if (this.cache[key] !== undefined) { + return this.cache[key] as T; } - return await this.getBypassCache(key, options); - } + const value = await this.getLocalSessionValue(await this.sessionKey.get(), key); - async getBypassCache(key: string, options?: MemoryStorageOptions): Promise { - const session = await this.getLocalSession(await this.getSessionEncKey()); - if (session == null || !Object.keys(session).includes(key)) { - return null; - } - - let value = session[key]; - if (options?.deserializer != null) { - value = options.deserializer(value as Jsonify); - } - - this.cache.set(key, value); - return this.cache.get(key) as T; + this.cache[key] = value; + return value as T; } async has(key: string): Promise { @@ -89,107 +79,139 @@ export class LocalBackedSessionStorageService } async save(key: string, obj: T): Promise { + // This is for observation purposes only. At some point, we don't want to write to local session storage if the value is the same. + if (this.platformUtilsService.isDev()) { + const existingValue = this.cache[key] as T; + try { + if (this.compareValues(existingValue, obj)) { + this.logService.warning( + `Possible unnecessary write to local session storage. Key: ${key}`, + ); + this.logService.warning(obj as any); + } + } catch (err) { + this.logService.warning(`Error while comparing values for key: ${key}`); + this.logService.warning(err); + } + } + if (obj == null) { return await this.remove(key); } - this.cache.set(key, obj); + this.cache[key] = obj; await this.updateLocalSessionValue(key, obj); - this.sendUpdate({ key, updateType: "save" }); + this.updatesSubject.next({ key, updateType: "save" }); } async remove(key: string): Promise { - this.cache.set(key, null); + this.cache[key] = null; await this.updateLocalSessionValue(key, null); - this.sendUpdate({ key, updateType: "remove" }); + this.updatesSubject.next({ key, updateType: "remove" }); } - sendUpdate(storageUpdate: StorageUpdate) { - this.updatesSubject.next(storageUpdate); - void chrome.runtime.sendMessage({ - command: this.commandName, - update: storageUpdate, - }); - } - - private async updateLocalSessionValue(key: string, obj: T) { - const sessionEncKey = await this.getSessionEncKey(); - const localSession = (await this.getLocalSession(sessionEncKey)) ?? {}; - localSession[key] = obj; - await this.setLocalSession(localSession, sessionEncKey); - } - - async getLocalSession(encKey: SymmetricCryptoKey): Promise> { - const local = await this.localStorage.get(this.sessionKey); - + private async getLocalSessionValue(encKey: SymmetricCryptoKey, key: string): Promise { + const local = await this.localStorage.get(this.sessionStorageKey(key)); if (local == null) { return null; } - if (devFlagEnabled("storeSessionDecrypted")) { - return local as any as Record; - } - - const sessionJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); - if (sessionJson == null) { - // Error with decryption -- session is lost, delete state and key and start over - await this.setSessionEncKey(null); - await this.localStorage.remove(this.sessionKey); + const valueJson = await this.encryptService.decryptToUtf8(new EncString(local), encKey); + if (valueJson == null) { + // error with decryption, value is lost, delete state and start over + await this.localStorage.remove(this.sessionStorageKey(key)); return null; } - return JSON.parse(sessionJson); + + return JSON.parse(valueJson); } - async setLocalSession(session: Record, key: SymmetricCryptoKey) { - if (devFlagEnabled("storeSessionDecrypted")) { - await this.setDecryptedLocalSession(session); - } else { - await this.setEncryptedLocalSession(session, key); + private async updateLocalSessionValue(key: string, value: unknown): Promise { + if (value == null) { + await this.localStorage.remove(this.sessionStorageKey(key)); + return; } + + const valueJson = JSON.stringify(value); + const encValue = await this.encryptService.encrypt(valueJson, await this.sessionKey.get()); + await this.localStorage.save(this.sessionStorageKey(key), encValue.encryptedString); } - @devFlag("storeSessionDecrypted") - async setDecryptedLocalSession(session: Record): Promise { - // Make sure we're storing the jsonified version of the session - const jsonSession = JSON.parse(JSON.stringify(session)); - if (session == null) { - await this.localStorage.remove(this.sessionKey); - } else { - await this.localStorage.save(this.sessionKey, jsonSession); + private async onMessageFromForeground( + message: MemoryStoragePortMessage, + port: chrome.runtime.Port, + ) { + if (message.originator === "background") { + return; } - } - async setEncryptedLocalSession(session: Record, key: SymmetricCryptoKey) { - const jsonSession = JSON.stringify(session); - const encSession = await this.encryptService.encrypt(jsonSession, key); + let result: unknown = null; - if (encSession == null) { - return await this.localStorage.remove(this.sessionKey); + switch (message.action) { + case "get": + case "has": { + result = await this[message.action](message.key); + break; + } + case "save": + await this.save(message.key, JSON.parse((message.data as string) ?? null) as unknown); + break; + case "remove": + await this.remove(message.key); + break; } - await this.localStorage.save(this.sessionKey, encSession.encryptedString); + + this.sendMessageTo(port, { + id: message.id, + key: message.key, + data: JSON.stringify(result), + }); } - async getSessionEncKey(): Promise { - let storedKey = await this.sessionStorage.get(this.encKey); - if (storedKey == null || Object.keys(storedKey).length == 0) { - const generatedKey = await this.keyGenerationService.createKeyWithPurpose( - 128, - "ephemeral", - "bitwarden-ephemeral", + protected broadcastMessage(data: Omit) { + this.ports.forEach((port) => { + this.sendMessageTo(port, data); + }); + } + + private sendMessageTo( + port: chrome.runtime.Port, + data: Omit, + ) { + port.postMessage({ + ...data, + originator: "background", + }); + } + + private sessionStorageKey(key: string) { + return `session_${key}`; + } + + private compareValues(value1: T, value2: T): boolean { + try { + if (value1 == null && value2 == null) { + return true; + } + + if (value1 && value2 == null) { + return false; + } + + if (value1 == null && value2) { + return false; + } + + if (typeof value1 !== "object" || typeof value2 !== "object") { + return value1 === value2; + } + + return JSON.stringify(value1) === JSON.stringify(value2); + } catch (e) { + this.logService.error( + `error comparing values\n${JSON.stringify(value1)}\n${JSON.stringify(value2)}`, ); - storedKey = generatedKey.derivedKey; - await this.setSessionEncKey(storedKey); - return storedKey; - } else { - return SymmetricCryptoKey.fromJSON(storedKey); - } - } - - async setSessionEncKey(input: SymmetricCryptoKey): Promise { - if (input == null) { - await this.sessionStorage.remove(this.encKey); - } else { - await this.sessionStorage.save(this.encKey, input); + return true; } } } diff --git a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts index 27ed3f016b..ec26d6aa29 100644 --- a/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/background-platform-utils.service.ts @@ -1,5 +1,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; + import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService { @@ -8,8 +10,9 @@ export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise, win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win); + super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); } override showToast( diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts index 0df8f26344..02c10b62cc 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.spec.ts @@ -1,15 +1,22 @@ +import { MockProxy, mock } from "jest-mock-extended"; + import { DeviceType } from "@bitwarden/common/enums"; import { flushPromises } from "../../../autofill/spec/testing-utils"; import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; import BrowserClipboardService from "../browser-clipboard.service"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { - constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) { - super(clipboardSpy, null, win); + constructor( + clipboardSpy: jest.Mock, + win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, + ) { + super(clipboardSpy, null, win, offscreenDocumentService); } showToast( @@ -24,13 +31,16 @@ class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService { describe("Browser Utils Service", () => { let browserPlatformUtilsService: BrowserPlatformUtilsService; + let offscreenDocumentService: MockProxy; const clipboardWriteCallbackSpy = jest.fn(); beforeEach(() => { + offscreenDocumentService = mock(); (window as any).matchMedia = jest.fn().mockReturnValueOnce({}); browserPlatformUtilsService = new TestBrowserPlatformUtilsService( clipboardWriteCallbackSpy, window, + offscreenDocumentService, ); }); @@ -223,23 +233,23 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); - jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(undefined); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); browserPlatformUtilsService.copyToClipboard(text); await flushPromises(); expect(triggerOffscreenCopyToClipboardSpy).toHaveBeenCalledWith(text); expect(clipboardServiceCopySpy).not.toHaveBeenCalled(); - expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith( + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( [chrome.offscreen.Reason.CLIPBOARD], "Write text to the clipboard.", + expect.any(Function), ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenCopyToClipboard", { text, }); - expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled(); }); it("skips the clipboardWriteCallback if the clipboard is clearing", async () => { @@ -298,18 +308,21 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); - jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue("test"); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve("test"), + ); await browserPlatformUtilsService.readFromClipboard(); - expect(BrowserApi.createOffscreenDocument).toHaveBeenCalledWith( + expect(offscreenDocumentService.withDocument).toHaveBeenCalledWith( [chrome.offscreen.Reason.CLIPBOARD], "Read text from the clipboard.", + expect.any(Function), ); + + const callback = offscreenDocumentService.withDocument.mock.calls[0][2]; + await callback(); expect(BrowserApi.sendMessageWithResponse).toHaveBeenCalledWith("offscreenReadFromClipboard"); - expect(BrowserApi.closeOffscreenDocument).toHaveBeenCalled(); }); it("returns an empty string from the offscreen document if the response is not of type string", async () => { @@ -317,9 +330,10 @@ describe("Browser Utils Service", () => { .spyOn(browserPlatformUtilsService, "getDevice") .mockReturnValue(DeviceType.ChromeExtension); getManifestVersionSpy.mockReturnValue(3); - jest.spyOn(BrowserApi, "createOffscreenDocument"); jest.spyOn(BrowserApi, "sendMessageWithResponse").mockResolvedValue(1); - jest.spyOn(BrowserApi, "closeOffscreenDocument"); + offscreenDocumentService.withDocument.mockImplementationOnce((_, __, callback) => + Promise.resolve(1), + ); const result = await browserPlatformUtilsService.readFromClipboard(); diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index e9f7f17d9b..855492521b 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -6,6 +6,7 @@ import { import { SafariApp } from "../../../browser/safariApp"; import { BrowserApi } from "../../browser/browser-api"; +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; import BrowserClipboardService from "../browser-clipboard.service"; export abstract class BrowserPlatformUtilsService implements PlatformUtilsService { @@ -15,6 +16,7 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, private biometricCallback: () => Promise, private globalContext: Window | ServiceWorkerGlobalScope, + private offscreenDocumentService: OffscreenDocumentService, ) {} static getDevice(globalContext: Window | ServiceWorkerGlobalScope): DeviceType { @@ -175,11 +177,13 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic } getApplicationVersion(): Promise { - return Promise.resolve(BrowserApi.getApplicationVersion()); + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version_name ?? manifest.version); } - async getApplicationVersionNumber(): Promise { - return (await this.getApplicationVersion()).split(RegExp("[+|-]"))[0].trim(); + getApplicationVersionNumber(): Promise { + const manifest = chrome.runtime.getManifest(); + return Promise.resolve(manifest.version.split(RegExp("[+|-]"))[0].trim()); } supportsWebAuthn(win: Window): boolean { @@ -314,24 +318,26 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic * Triggers the offscreen document API to copy the text to the clipboard. */ private async triggerOffscreenCopyToClipboard(text: string) { - await BrowserApi.createOffscreenDocument( + await this.offscreenDocumentService.withDocument( [chrome.offscreen.Reason.CLIPBOARD], "Write text to the clipboard.", + async () => { + await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); + }, ); - await BrowserApi.sendMessageWithResponse("offscreenCopyToClipboard", { text }); - BrowserApi.closeOffscreenDocument(); } /** * Triggers the offscreen document API to read the text from the clipboard. */ private async triggerOffscreenReadFromClipboard() { - await BrowserApi.createOffscreenDocument( + const response = await this.offscreenDocumentService.withDocument( [chrome.offscreen.Reason.CLIPBOARD], "Read text from the clipboard.", + async () => { + return await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); + }, ); - const response = await BrowserApi.sendMessageWithResponse("offscreenReadFromClipboard"); - BrowserApi.closeOffscreenDocument(); if (typeof response === "string") { return response; } diff --git a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts index 8cf1a8d3e4..f775f049e7 100644 --- a/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/foreground-platform-utils.service.ts @@ -1,18 +1,18 @@ -import { SecurityContext } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; -import { ToastrService } from "ngx-toastr"; +import { ToastService } from "@bitwarden/components"; + +import { OffscreenDocumentService } from "../../offscreen-document/abstractions/offscreen-document"; import { BrowserPlatformUtilsService } from "./browser-platform-utils.service"; export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService { constructor( - private sanitizer: DomSanitizer, - private toastrService: ToastrService, + private toastService: ToastService, clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void, biometricCallback: () => Promise, win: Window & typeof globalThis, + offscreenDocumentService: OffscreenDocumentService, ) { - super(clipboardWriteCallback, biometricCallback, win); + super(clipboardWriteCallback, biometricCallback, win, offscreenDocumentService); } override showToast( @@ -21,20 +21,6 @@ export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService text: string | string[], options?: any, ): void { - if (typeof text === "string") { - // Already in the correct format - } else if (text.length === 1) { - text = text[0]; - } else { - let message = ""; - text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - text = message; - options.enableHtml = true; - } - this.toastrService.show(text, title, options, "toast-" + type); - // noop + this.toastService._showToast({ type, title, text, options }); } } diff --git a/apps/browser/src/platform/state/background-derived-state.provider.ts b/apps/browser/src/platform/state/background-derived-state.provider.ts index 95eec71113..cbc5a34b37 100644 --- a/apps/browser/src/platform/state/background-derived-state.provider.ts +++ b/apps/browser/src/platform/state/background-derived-state.provider.ts @@ -16,7 +16,7 @@ export class BackgroundDerivedStateProvider extends DefaultDerivedStateProvider return new BackgroundDerivedState( parentState$, deriveDefinition, - this.memoryStorage, + deriveDefinition.buildCacheKey(), dependencies, ); } diff --git a/apps/browser/src/platform/state/background-derived-state.ts b/apps/browser/src/platform/state/background-derived-state.ts index 7a7146aa88..61768cb970 100644 --- a/apps/browser/src/platform/state/background-derived-state.ts +++ b/apps/browser/src/platform/state/background-derived-state.ts @@ -1,10 +1,7 @@ -import { Observable, Subscription } from "rxjs"; +import { Observable, Subscription, concatMap } from "rxjs"; import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedState } from "@bitwarden/common/platform/state/implementations/default-derived-state"; @@ -22,11 +19,10 @@ export class BackgroundDerivedState< constructor( parentState$: Observable, deriveDefinition: DeriveDefinition, - memoryStorage: AbstractStorageService & ObservableStorageService, + portName: string, dependencies: TDeps, ) { - super(parentState$, deriveDefinition, memoryStorage, dependencies); - const portName = deriveDefinition.buildCacheKey(); + super(parentState$, deriveDefinition, dependencies); // listen for foreground derived states to connect BrowserApi.addListener(chrome.runtime.onConnect, (port) => { @@ -42,7 +38,20 @@ export class BackgroundDerivedState< }); port.onMessage.addListener(listenerCallback); - const stateSubscription = this.state$.subscribe(); + const stateSubscription = this.state$ + .pipe( + concatMap(async (state) => { + await this.sendMessage( + { + action: "nextState", + data: JSON.stringify(state), + id: Utils.newGuid(), + }, + port, + ); + }), + ) + .subscribe(); this.portSubscriptions.set(port, stateSubscription); }); diff --git a/apps/browser/src/platform/state/derived-state-interactions.spec.ts b/apps/browser/src/platform/state/derived-state-interactions.spec.ts index d709c401af..823c071a4c 100644 --- a/apps/browser/src/platform/state/derived-state-interactions.spec.ts +++ b/apps/browser/src/platform/state/derived-state-interactions.spec.ts @@ -4,14 +4,13 @@ */ import { NgZone } from "@angular/core"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec/utils"; import { mock } from "jest-mock-extended"; import { Subject, firstValueFrom } from "rxjs"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- needed to define a derive definition import { StateDefinition } from "@bitwarden/common/platform/state/state-definition"; +import { awaitAsync, trackEmissions, ObservableTracker } from "@bitwarden/common/spec"; import { mockPorts } from "../../../spec/mock-port.spec-util"; @@ -22,6 +21,7 @@ const stateDefinition = new StateDefinition("test", "memory"); const deriveDefinition = new DeriveDefinition(stateDefinition, "test", { derive: (dateString: string) => (dateString == null ? null : new Date(dateString)), deserializer: (dateString: string) => (dateString == null ? null : new Date(dateString)), + cleanupDelayMs: 1000, }); // Mock out the runInsideAngular operator so we don't have to deal with zone.js @@ -35,17 +35,16 @@ describe("foreground background derived state interactions", () => { let foreground: ForegroundDerivedState; let background: BackgroundDerivedState>; let parentState$: Subject; - let memoryStorage: FakeStorageService; const initialParent = "2020-01-01"; const ngZone = mock(); + const portName = "testPort"; beforeEach(() => { mockPorts(); parentState$ = new Subject(); - memoryStorage = new FakeStorageService(); - background = new BackgroundDerivedState(parentState$, deriveDefinition, memoryStorage, {}); - foreground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + background = new BackgroundDerivedState(parentState$, deriveDefinition, portName, {}); + foreground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -65,16 +64,13 @@ describe("foreground background derived state interactions", () => { }); it("should initialize a late-connected foreground", async () => { - const newForeground = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); - const backgroundEmissions = trackEmissions(background.state$); + const newForeground = new ForegroundDerivedState(deriveDefinition, portName, ngZone); + const backgroundTracker = new ObservableTracker(background.state$); parentState$.next(initialParent); - await awaitAsync(); + const foregroundTracker = new ObservableTracker(newForeground.state$); - const foregroundEmissions = trackEmissions(newForeground.state$); - await awaitAsync(10); - - expect(backgroundEmissions).toEqual([new Date(initialParent)]); - expect(foregroundEmissions).toEqual([new Date(initialParent)]); + expect(await backgroundTracker.expectEmission()).toEqual(new Date(initialParent)); + expect(await foregroundTracker.expectEmission()).toEqual(new Date(initialParent)); }); describe("forceValue", () => { @@ -82,8 +78,6 @@ describe("foreground background derived state interactions", () => { const dateString = "2020-12-12"; const emissions = trackEmissions(background.state$); - // 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 await foreground.forceValue(new Date(dateString)); await awaitAsync(); @@ -99,9 +93,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeDefined(); const newDate = new Date(); - // 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 - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls); @@ -114,9 +106,7 @@ describe("foreground background derived state interactions", () => { expect(foreground["port"]).toBeUndefined(); const newDate = new Date(); - // 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 - foreground.forceValue(newDate); + await foreground.forceValue(newDate); await awaitAsync(); expect(connectMock.mock.calls.length).toBe(initialConnectCalls + 1); diff --git a/apps/browser/src/platform/state/foreground-derived-state.provider.ts b/apps/browser/src/platform/state/foreground-derived-state.provider.ts index ccefb1157c..8b8d82b914 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.provider.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.provider.ts @@ -1,10 +1,6 @@ import { NgZone } from "@angular/core"; import { Observable } from "rxjs"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- extending this class for this client import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider"; @@ -13,17 +9,18 @@ import { DerivedStateDependencies } from "@bitwarden/common/src/types/state"; import { ForegroundDerivedState } from "./foreground-derived-state"; export class ForegroundDerivedStateProvider extends DefaultDerivedStateProvider { - constructor( - memoryStorage: AbstractStorageService & ObservableStorageService, - private ngZone: NgZone, - ) { - super(memoryStorage); + constructor(private ngZone: NgZone) { + super(); } override buildDerivedState( _parentState$: Observable, deriveDefinition: DeriveDefinition, _dependencies: TDeps, ): DerivedState { - return new ForegroundDerivedState(deriveDefinition, this.memoryStorage, this.ngZone); + return new ForegroundDerivedState( + deriveDefinition, + deriveDefinition.buildCacheKey(), + this.ngZone, + ); } } diff --git a/apps/browser/src/platform/state/foreground-derived-state.spec.ts b/apps/browser/src/platform/state/foreground-derived-state.spec.ts index fce672a5ef..ee224540c1 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.spec.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.spec.ts @@ -1,11 +1,5 @@ -/** - * need to update test environment so structuredClone works appropriately - * @jest-environment ../../libs/shared/test.environment.ts - */ - import { NgZone } from "@angular/core"; -import { awaitAsync, trackEmissions } from "@bitwarden/common/../spec"; -import { FakeStorageService } from "@bitwarden/common/../spec/fake-storage.service"; +import { awaitAsync } from "@bitwarden/common/../spec"; import { mock } from "jest-mock-extended"; import { DeriveDefinition } from "@bitwarden/common/platform/state"; @@ -32,14 +26,12 @@ jest.mock("../browser/run-inside-angular.operator", () => { describe("ForegroundDerivedState", () => { let sut: ForegroundDerivedState; - let memoryStorage: FakeStorageService; + const portName = "testPort"; const ngZone = mock(); beforeEach(() => { - memoryStorage = new FakeStorageService(); - memoryStorage.internalUpdateValuesRequireDeserialization(true); mockPorts(); - sut = new ForegroundDerivedState(deriveDefinition, memoryStorage, ngZone); + sut = new ForegroundDerivedState(deriveDefinition, portName, ngZone); }); afterEach(() => { @@ -66,18 +58,4 @@ describe("ForegroundDerivedState", () => { expect(disconnectSpy).toHaveBeenCalled(); expect(sut["port"]).toBeNull(); }); - - it("should emit when the memory storage updates", async () => { - const dateString = "2020-01-01"; - const emissions = trackEmissions(sut.state$); - - await memoryStorage.save(deriveDefinition.storageKey, { - derived: true, - value: new Date(dateString), - }); - - await awaitAsync(); - - expect(emissions).toEqual([new Date(dateString)]); - }); }); diff --git a/apps/browser/src/platform/state/foreground-derived-state.ts b/apps/browser/src/platform/state/foreground-derived-state.ts index b005697be8..6abe363876 100644 --- a/apps/browser/src/platform/state/foreground-derived-state.ts +++ b/apps/browser/src/platform/state/foreground-derived-state.ts @@ -6,19 +6,14 @@ import { filter, firstValueFrom, map, - merge, of, share, switchMap, tap, timer, } from "rxjs"; -import { Jsonify, JsonObject } from "type-fest"; +import { Jsonify } from "type-fest"; -import { - AbstractStorageService, - ObservableStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DeriveDefinition, DerivedState } from "@bitwarden/common/platform/state"; import { DerivedStateDependencies } from "@bitwarden/common/types/state"; @@ -27,40 +22,28 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { runInsideAngular } from "../browser/run-inside-angular.operator"; export class ForegroundDerivedState implements DerivedState { - private storageKey: string; private port: chrome.runtime.Port; private backgroundResponses$: Observable; state$: Observable; constructor( private deriveDefinition: DeriveDefinition, - private memoryStorage: AbstractStorageService & ObservableStorageService, + private portName: string, private ngZone: NgZone, ) { - this.storageKey = deriveDefinition.storageKey; - - const initialStorageGet$ = defer(() => { - return this.getStoredValue(); - }).pipe( - filter((s) => s.derived), - map((s) => s.value), - ); - - const latestStorage$ = this.memoryStorage.updates$.pipe( - filter((s) => s.key === this.storageKey), - switchMap(async (storageUpdate) => { - if (storageUpdate.updateType === "remove") { - return null; - } - - return await this.getStoredValue(); - }), - filter((s) => s.derived), - map((s) => s.value), - ); + const latestValueFromPort$ = (port: chrome.runtime.Port) => { + return fromChromeEvent(port.onMessage).pipe( + map(([message]) => message as DerivedStateMessage), + filter((message) => message.originator === "background" && message.action === "nextState"), + map((message) => { + const json = JSON.parse(message.data) as Jsonify; + return this.deriveDefinition.deserialize(json); + }), + ); + }; this.state$ = defer(() => of(this.initializePort())).pipe( - switchMap(() => merge(initialStorageGet$, latestStorage$)), + switchMap(() => latestValueFromPort$(this.port)), share({ connector: () => new ReplaySubject(1), resetOnRefCountZero: () => @@ -88,7 +71,7 @@ export class ForegroundDerivedState implements DerivedState { return; } - this.port = chrome.runtime.connect({ name: this.deriveDefinition.buildCacheKey() }); + this.port = chrome.runtime.connect({ name: this.portName }); this.backgroundResponses$ = fromChromeEvent(this.port.onMessage).pipe( map(([message]) => message as DerivedStateMessage), @@ -129,28 +112,4 @@ export class ForegroundDerivedState implements DerivedState { this.port = null; this.backgroundResponses$ = null; } - - protected async getStoredValue(): Promise<{ derived: boolean; value: TTo | null }> { - if (this.memoryStorage.valuesRequireDeserialization) { - const storedJson = await this.memoryStorage.get< - Jsonify<{ derived: true; value: JsonObject }> - >(this.storageKey); - - if (!storedJson?.derived) { - return { derived: false, value: null }; - } - - const value = this.deriveDefinition.deserialize(storedJson.value as any); - - return { derived: true, value }; - } else { - const stored = await this.memoryStorage.get<{ derived: true; value: TTo }>(this.storageKey); - - if (!stored?.derived) { - return { derived: false, value: null }; - } - - return { derived: true, value: stored.value }; - } - } } diff --git a/apps/browser/src/platform/storage/background-memory-storage.service.ts b/apps/browser/src/platform/storage/background-memory-storage.service.ts index 9203d2aacb..a1d333affa 100644 --- a/apps/browser/src/platform/storage/background-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/background-memory-storage.service.ts @@ -51,7 +51,6 @@ export class BackgroundMemoryStorageService extends MemoryStorageService { switch (message.action) { case "get": - case "getBypassCache": case "has": { result = await this[message.action](message.key); break; diff --git a/apps/browser/src/platform/storage/browser-storage-service.provider.ts b/apps/browser/src/platform/storage/browser-storage-service.provider.ts new file mode 100644 index 0000000000..e0214baef4 --- /dev/null +++ b/apps/browser/src/platform/storage/browser-storage-service.provider.ts @@ -0,0 +1,35 @@ +import { + AbstractStorageService, + ObservableStorageService, +} from "@bitwarden/common/platform/abstractions/storage.service"; +import { + PossibleLocation, + StorageServiceProvider, +} from "@bitwarden/common/platform/services/storage-service.provider"; +// eslint-disable-next-line import/no-restricted-paths +import { ClientLocations } from "@bitwarden/common/platform/state/state-definition"; + +export class BrowserStorageServiceProvider extends StorageServiceProvider { + constructor( + diskStorageService: AbstractStorageService & ObservableStorageService, + limitedMemoryStorageService: AbstractStorageService & ObservableStorageService, + private largeObjectMemoryStorageService: AbstractStorageService & ObservableStorageService, + ) { + super(diskStorageService, limitedMemoryStorageService); + } + + override get( + defaultLocation: PossibleLocation, + overrides: Partial, + ): [location: PossibleLocation, service: AbstractStorageService & ObservableStorageService] { + const location = overrides["browser"] ?? defaultLocation; + switch (location) { + case "memory-large-object": + return ["memory-large-object", this.largeObjectMemoryStorageService]; + default: + // Pass in computed location to super because they could have + // override default "disk" with web "memory". + return super.get(location, overrides); + } + } +} diff --git a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts index 1e5220002a..bd6a52c82f 100644 --- a/apps/browser/src/platform/storage/foreground-memory-storage.service.ts +++ b/apps/browser/src/platform/storage/foreground-memory-storage.service.ts @@ -1,7 +1,7 @@ import { Observable, Subject, filter, firstValueFrom, map } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -11,7 +11,7 @@ import { fromChromeEvent } from "../browser/from-chrome-event"; import { MemoryStoragePortMessage } from "./port-messages"; import { portName } from "./port-name"; -export class ForegroundMemoryStorageService extends AbstractMemoryStorageService { +export class ForegroundMemoryStorageService extends AbstractStorageService { private _port: chrome.runtime.Port; private _backgroundResponses$: Observable; private updatesSubject = new Subject(); @@ -21,12 +21,16 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService } updates$; - constructor() { + constructor(private partitionName?: string) { super(); this.updates$ = this.updatesSubject.asObservable(); - this._port = chrome.runtime.connect({ name: portName(chrome.storage.session) }); + let name = portName(chrome.storage.session); + if (this.partitionName) { + name = `${name}_${this.partitionName}`; + } + this._port = chrome.runtime.connect({ name }); this._backgroundResponses$ = fromChromeEvent(this._port.onMessage).pipe( map(([message]) => message), filter((message) => message.originator === "background"), @@ -55,9 +59,6 @@ export class ForegroundMemoryStorageService extends AbstractMemoryStorageService async get(key: string): Promise { return await this.delegateToBackground("get", key); } - async getBypassCache(key: string): Promise { - return await this.delegateToBackground("getBypassCache", key); - } async has(key: string): Promise { return await this.delegateToBackground("has", key); } diff --git a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts index 43ffb6a065..c462f24269 100644 --- a/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts +++ b/apps/browser/src/platform/storage/memory-storage-service-interactions.spec.ts @@ -25,9 +25,9 @@ describe("foreground background memory storage interaction", () => { jest.resetAllMocks(); }); - test.each(["has", "get", "getBypassCache"])( + test.each(["has", "get"])( "background should respond with the correct value for %s", - async (action: "get" | "has" | "getBypassCache") => { + async (action: "get" | "has") => { const key = "key"; const value = "value"; background[action] = jest.fn().mockResolvedValue(value); diff --git a/apps/browser/src/platform/storage/port-messages.d.ts b/apps/browser/src/platform/storage/port-messages.d.ts index a64a9b2ef7..60817c98a4 100644 --- a/apps/browser/src/platform/storage/port-messages.d.ts +++ b/apps/browser/src/platform/storage/port-messages.d.ts @@ -1,5 +1,5 @@ import { - AbstractMemoryStorageService, + AbstractStorageService, StorageUpdate, } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -14,7 +14,7 @@ type MemoryStoragePortMessage = { data: string | string[] | StorageUpdate; originator: "foreground" | "background"; action?: - | keyof Pick + | keyof Pick | "subject_update" | "initialization"; }; diff --git a/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts new file mode 100644 index 0000000000..e30f35b680 --- /dev/null +++ b/apps/browser/src/platform/utils/from-chrome-runtime-messaging.ts @@ -0,0 +1,26 @@ +import { map, share } from "rxjs"; + +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +import { fromChromeEvent } from "../browser/from-chrome-event"; + +/** + * Creates an observable that listens to messages through `chrome.runtime.onMessage`. + * @returns An observable stream of messages. + */ +export const fromChromeRuntimeMessaging = () => { + return fromChromeEvent(chrome.runtime.onMessage).pipe( + map(([message, sender]) => { + message ??= {}; + + // Force the sender onto the message as long as we won't overwrite anything + if (!("webExtSender" in message)) { + message.webExtSender = sender; + } + + return message; + }), + tagAsExternal, + share(), + ); +}; diff --git a/apps/browser/src/popup/app-routing.animations.ts b/apps/browser/src/popup/app-routing.animations.ts index 13403545fd..9bad33f744 100644 --- a/apps/browser/src/popup/app-routing.animations.ts +++ b/apps/browser/src/popup/app-routing.animations.ts @@ -174,20 +174,27 @@ export const routerTransition = trigger("routerTransition", [ transition("clone-cipher => attachments, clone-cipher => collections", inSlideLeft), transition("attachments => clone-cipher, collections => clone-cipher", outSlideRight), - transition("tabs => import", inSlideLeft), - transition("import => tabs", outSlideRight), + transition("tabs => account-security", inSlideLeft), + transition("account-security => tabs", outSlideRight), - transition("tabs => export", inSlideLeft), - transition("export => tabs", outSlideRight), + // Vault settings + transition("tabs => vault-settings", inSlideLeft), + transition("vault-settings => tabs", outSlideRight), - transition("tabs => folders", inSlideLeft), - transition("folders => tabs", outSlideRight), + transition("vault-settings => import", inSlideLeft), + transition("import => vault-settings", outSlideRight), + + transition("vault-settings => export", inSlideLeft), + transition("export => vault-settings", outSlideRight), + + transition("vault-settings => folders", inSlideLeft), + transition("folders => vault-settings", outSlideRight), transition("folders => edit-folder, folders => add-folder", inSlideUp), transition("edit-folder => folders, add-folder => folders", outSlideDown), - transition("tabs => sync", inSlideLeft), - transition("sync => tabs", outSlideRight), + transition("vault-settings => sync", inSlideLeft), + transition("sync => vault-settings", outSlideRight), transition("tabs => excluded-domains", inSlideLeft), transition("excluded-domains => tabs", outSlideRight), diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index ac402e9583..c4e9acbd75 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -2,9 +2,9 @@ import { Injectable, NgModule } from "@angular/core"; import { ActivatedRouteSnapshot, RouteReuseStrategy, RouterModule, Routes } from "@angular/router"; import { - redirectGuard, AuthGuard, lockGuard, + redirectGuard, tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; @@ -21,11 +21,13 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; import { SsoComponent } from "../auth/popup/sso.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 { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { PremiumComponent } from "../billing/popup/settings/premium.component"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -34,6 +36,7 @@ import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.compo import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; import { ImportBrowserComponent } from "../tools/popup/settings/import/import-browser.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { Fido2Component } from "../vault/popup/components/fido2/fido2.component"; import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component"; import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component"; @@ -45,15 +48,16 @@ import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filt import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; +import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { PremiumComponent } from "./settings/premium.component"; -import { SettingsComponent } from "./settings/settings.component"; -import { SyncComponent } from "./settings/sync.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; const unauthRouteOverrides = { @@ -244,6 +248,18 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "autofill" }, }, + { + path: "account-security", + component: AccountSecurityComponent, + canActivate: [AuthGuard], + data: { state: "account-security" }, + }, + { + path: "vault-settings", + component: VaultSettingsComponent, + canActivate: [AuthGuard], + data: { state: "vault-settings" }, + }, { path: "folders", component: FoldersComponent, @@ -322,9 +338,8 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "help-and-feedback" }, }, - { + ...extensionRefreshSwap(TabsComponent, TabsV2Component, { path: "tabs", - component: TabsComponent, data: { state: "tabs" }, children: [ { @@ -336,6 +351,7 @@ const routes: Routes = [ path: "current", component: CurrentTabComponent, canActivate: [AuthGuard], + canMatch: [extensionRefreshRedirect("/tabs/vault")], data: { state: "tabs_current" }, runGuardsAndResolvers: "always", }, @@ -364,7 +380,7 @@ const routes: Routes = [ data: { state: "tabs_send" }, }, ], - }, + }), { path: "account-switcher", component: AccountSwitcherComponent, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index c224e652f6..25fac44450 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -1,19 +1,19 @@ import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { NavigationEnd, Router, RouterOutlet } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; -import { filter, concatMap, Subject, takeUntil, firstValueFrom, map } from "rxjs"; +import { Subject, takeUntil, firstValueFrom, concatMap, filter, tap } 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 { 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 { MessageListener } from "@bitwarden/common/platform/messaging"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; -import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service"; import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; -import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -29,14 +29,13 @@ import { DesktopSyncVerificationDialogComponent } from "./components/desktop-syn `, }) export class AppComponent implements OnInit, OnDestroy { - private lastActivity: number = null; - private activeUserId: string; + private lastActivity: Date; + private activeUserId: UserId; + private recordActivitySubject = new Subject(); private destroy$ = new Subject(); constructor( - private toastrService: ToastrService, - private broadcasterService: BroadcasterService, private authService: AuthService, private i18nService: I18nService, private router: Router, @@ -46,9 +45,11 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private changeDetectorRef: ChangeDetectorRef, private ngZone: NgZone, - private platformUtilsService: ForegroundPlatformUtilsService, + private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, - private browserMessagingApi: ZonedMessageListenerService, + private messageListener: MessageListener, + private toastService: ToastService, + private accountService: AccountService, ) {} async ngOnInit() { @@ -56,14 +57,13 @@ export class AppComponent implements OnInit, OnDestroy { // Clear them aggressively to make sure this doesn't occur await this.clearComponentStates(); - this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => { - this.activeUserId = userId; + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { + this.activeUserId = account?.id; }); this.authService.activeAccountStatus$ .pipe( - map((status) => status === AuthenticationStatus.Unlocked), - filter((unlocked) => unlocked), + filter((status) => status === AuthenticationStatus.Unlocked), concatMap(async () => { await this.recordActivity(); }), @@ -79,77 +79,76 @@ export class AppComponent implements OnInit, OnDestroy { window.onkeypress = () => this.recordActivity(); }); - const bitwardenPopupMainMessageListener = (msg: any, sender: any) => { - if (msg.command === "doneLoggingOut") { - this.authService.logOut(async () => { - if (msg.expired) { - this.showToast({ - type: "warning", - title: this.i18nService.t("loggedOut"), - text: this.i18nService.t("loginExpired"), + this.messageListener.allMessages$ + .pipe( + tap((msg: any) => { + if (msg.command === "doneLoggingOut") { + this.authService.logOut(async () => { + if (msg.expired) { + this.toastService.showToast({ + variant: "warning", + title: this.i18nService.t("loggedOut"), + message: this.i18nService.t("loginExpired"), + }); + } + + // 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(["home"]); }); + this.changeDetectorRef.detectChanges(); + } else if (msg.command === "authBlocked") { + // 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(["home"]); + } else if ( + msg.command === "locked" && + (msg.userId == null || msg.userId == this.activeUserId) + ) { + // 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"]); + } else if (msg.command === "showDialog") { + // 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.showDialog(msg); + } else if (msg.command === "showNativeMessagingFinterprintDialog") { + // TODO: Should be refactored to live in another service. + // 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.showNativeMessagingFingerprintDialog(msg); + } else if (msg.command === "showToast") { + this.toastService._showToast(msg); + } else if (msg.command === "reloadProcess") { + const forceWindowReload = + this.platformUtilsService.isSafari() || + this.platformUtilsService.isFirefox() || + this.platformUtilsService.isOpera(); + // Wait to make sure background has reloaded first. + window.setTimeout( + () => BrowserApi.reloadExtension(forceWindowReload ? window : null), + 2000, + ); + } else if (msg.command === "reloadPopup") { + // 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(["/"]); + } else if (msg.command === "convertAccountToKeyConnector") { + // 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(["/remove-password"]); + } else if (msg.command === "switchAccountFinish") { + // TODO: unset loading? + // this.loading = false; + } else if (msg.command == "update-temp-password") { + // 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(["/update-temp-password"]); } - - // 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(["home"]); - }); - this.changeDetectorRef.detectChanges(); - } else if (msg.command === "authBlocked") { - // 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(["home"]); - } else if ( - msg.command === "locked" && - (msg.userId == null || msg.userId == this.activeUserId) - ) { - // 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"]); - } else if (msg.command === "showDialog") { - // 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.showDialog(msg); - } else if (msg.command === "showNativeMessagingFinterprintDialog") { - // TODO: Should be refactored to live in another service. - // 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.showNativeMessagingFingerprintDialog(msg); - } else if (msg.command === "showToast") { - this.showToast(msg); - } else if (msg.command === "reloadProcess") { - const forceWindowReload = - this.platformUtilsService.isSafari() || - this.platformUtilsService.isFirefox() || - this.platformUtilsService.isOpera(); - // Wait to make sure background has reloaded first. - window.setTimeout( - () => BrowserApi.reloadExtension(forceWindowReload ? window : null), - 2000, - ); - } else if (msg.command === "reloadPopup") { - // 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(["/"]); - } else if (msg.command === "convertAccountToKeyConnector") { - // 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(["/remove-password"]); - } else if (msg.command === "switchAccountFinish") { - // TODO: unset loading? - // this.loading = false; - } else if (msg.command == "update-temp-password") { - // 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(["/update-temp-password"]); - } else { - msg.webExtSender = sender; - this.broadcasterService.send(msg); - } - }; - - (self as any).bitwardenPopupMainMessageListener = bitwardenPopupMainMessageListener; - this.browserMessagingApi.messageListener("app.component", bitwardenPopupMainMessageListener); + }), + takeUntil(this.destroy$), + ) + .subscribe(); // eslint-disable-next-line rxjs/no-async-subscribe this.router.events.pipe(takeUntil(this.destroy$)).subscribe(async (event) => { @@ -204,13 +203,13 @@ export class AppComponent implements OnInit, OnDestroy { return; } - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - await this.stateService.setLastActive(now, { userId: this.activeUserId }); + await this.accountService.setAccountActivity(this.activeUserId, now); } private showToast(msg: any) { diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index d179868448..71e6ed4f17 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -11,11 +11,10 @@ import { BrowserModule } from "@angular/platform-browser"; import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { BitwardenToastModule } from "@bitwarden/angular/components/toastr.component"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; -import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { AvatarModule, ButtonModule, ToastModule } from "@bitwarden/components"; import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; @@ -31,12 +30,19 @@ import { LoginComponent } from "../auth/popup/login.component"; import { RegisterComponent } from "../auth/popup/register.component"; import { RemovePasswordComponent } from "../auth/popup/remove-password.component"; import { SetPasswordComponent } from "../auth/popup/set-password.component"; +import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component"; +import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component"; import { SsoComponent } from "../auth/popup/sso.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 { AutofillComponent } from "../autofill/popup/settings/autofill.component"; +import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { HeaderComponent } from "../platform/popup/header.component"; +import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.component"; +import { PopupPageComponent } from "../platform/popup/layout/popup-page.component"; +import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component"; import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { PasswordGeneratorHistoryComponent } from "../tools/popup/generator/password-generator-history.component"; @@ -45,6 +51,7 @@ import { SendAddEditComponent } from "../tools/popup/send/send-add-edit.componen import { SendGroupingsComponent } from "../tools/popup/send/send-groupings.component"; import { SendTypeComponent } from "../tools/popup/send/send-type.component"; import { ExportComponent } from "../tools/popup/settings/export.component"; +import { SettingsComponent } from "../tools/popup/settings/settings.component"; import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component"; import { CipherRowComponent } from "../vault/popup/components/cipher-row.component"; import { Fido2CipherRowComponent } from "../vault/popup/components/fido2/fido2-cipher-row.component"; @@ -63,21 +70,19 @@ import { VaultSelectComponent } from "../vault/popup/components/vault/vault-sele import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; +import { FoldersComponent } from "../vault/popup/settings/folders.component"; +import { SyncComponent } from "../vault/popup/settings/sync.component"; +import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component"; import { AppRoutingModule } from "./app-routing.module"; import { AppComponent } from "./app.component"; import { PopOutComponent } from "./components/pop-out.component"; -import { PrivateModeWarningComponent } from "./components/private-mode-warning.component"; import { UserVerificationComponent } from "./components/user-verification.component"; import { ServicesModule } from "./services/services.module"; import { ExcludedDomainsComponent } from "./settings/excluded-domains.component"; -import { FoldersComponent } from "./settings/folders.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { OptionsComponent } from "./settings/options.component"; -import { PremiumComponent } from "./settings/premium.component"; -import { SettingsComponent } from "./settings/settings.component"; -import { SyncComponent } from "./settings/sync.component"; -import { VaultTimeoutInputComponent } from "./settings/vault-timeout-input.component"; +import { TabsV2Component } from "./tabs-v2.component"; import { TabsComponent } from "./tabs.component"; // Register the locales for the application @@ -87,7 +92,7 @@ import "../platform/popup/locales"; imports: [ A11yModule, AppRoutingModule, - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 2, autoDismiss: true, closeButton: true, @@ -109,6 +114,10 @@ import "../platform/popup/locales"; AccountComponent, ButtonModule, ExportScopeCalloutComponent, + PopupPageComponent, + PopupTabNavigationComponent, + PopupFooterComponent, + PopupHeaderComponent, ], declarations: [ ActionButtonsComponent, @@ -143,18 +152,20 @@ import "../platform/popup/locales"; PasswordHistoryComponent, PopOutComponent, PremiumComponent, - PrivateModeWarningComponent, RegisterComponent, SendAddEditComponent, SendGroupingsComponent, SendListComponent, SendTypeComponent, SetPasswordComponent, + AccountSecurityComponent, SettingsComponent, + VaultSettingsComponent, ShareComponent, SsoComponent, SyncComponent, TabsComponent, + TabsV2Component, TwoFactorComponent, TwoFactorOptionsComponent, UpdateTempPasswordComponent, diff --git a/apps/browser/src/popup/components/private-mode-warning.component.html b/apps/browser/src/popup/components/private-mode-warning.component.html deleted file mode 100644 index 9cd53ea1be..0000000000 --- a/apps/browser/src/popup/components/private-mode-warning.component.html +++ /dev/null @@ -1,6 +0,0 @@ - - {{ "privateModeWarning" | i18n }} - {{ - "learnMore" | i18n - }} - diff --git a/apps/browser/src/popup/components/private-mode-warning.component.ts b/apps/browser/src/popup/components/private-mode-warning.component.ts deleted file mode 100644 index ff6292bdbf..0000000000 --- a/apps/browser/src/popup/components/private-mode-warning.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from "@angular/core"; - -import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; - -@Component({ - selector: "app-private-mode-warning", - templateUrl: "private-mode-warning.component.html", -}) -export class PrivateModeWarningComponent implements OnInit { - showWarning = false; - - ngOnInit() { - this.showWarning = BrowserPopupUtils.inPrivateMode(); - } -} diff --git a/apps/browser/src/popup/extension-refresh-route-utils.ts b/apps/browser/src/popup/extension-refresh-route-utils.ts new file mode 100644 index 0000000000..3c2ca33f86 --- /dev/null +++ b/apps/browser/src/popup/extension-refresh-route-utils.ts @@ -0,0 +1,45 @@ +import { inject, Type } from "@angular/core"; +import { Route, Router, Routes, UrlTree } from "@angular/router"; + +import { componentRouteSwap } from "@bitwarden/angular/utils/component-route-swap"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +/** + * Helper function to swap between two components based on the ExtensionRefresh feature flag. + * @param defaultComponent - The current non-refreshed component to render. + * @param refreshedComponent - The new refreshed component to render. + * @param options - The shared route options to apply to both components. + */ +export function extensionRefreshSwap( + defaultComponent: Type, + refreshedComponent: Type, + options: Route, +): Routes { + return componentRouteSwap( + defaultComponent, + refreshedComponent, + async () => { + const configService = inject(ConfigService); + return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + }, + options, + ); +} + +/** + * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag. + * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled. + */ +export function extensionRefreshRedirect(redirectUrl: string): () => Promise { + return async () => { + const configService = inject(ConfigService); + const router = inject(Router); + const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh); + if (shouldRedirect) { + return router.parseUrl(redirectUrl); + } else { + return true; + } + }; +} diff --git a/apps/browser/src/popup/scss/base.scss b/apps/browser/src/popup/scss/base.scss index 73da455941..80c7536087 100644 --- a/apps/browser/src/popup/scss/base.scss +++ b/apps/browser/src/popup/scss/base.scss @@ -68,7 +68,7 @@ img { border: none; } -a { +a:not(popup-page a, popup-tab-navigation a) { text-decoration: none; @include themify($themes) { @@ -171,7 +171,7 @@ cdk-virtual-scroll-viewport::-webkit-scrollbar-thumb, } } -header:not(bit-callout header, bit-dialog header) { +header:not(bit-callout header, bit-dialog header, popup-page header) { height: 44px; display: flex; @@ -448,7 +448,7 @@ app-root { } } -main { +main:not(popup-page main) { position: absolute; top: 44px; bottom: 0; diff --git a/apps/browser/src/popup/scss/pages.scss b/apps/browser/src/popup/scss/pages.scss index 3c651682c1..3ae3647299 100644 --- a/apps/browser/src/popup/scss/pages.scss +++ b/apps/browser/src/popup/scss/pages.scss @@ -111,11 +111,6 @@ app-home { } } -.app-private-mode-warning { - display: block; - padding-top: 1rem; -} - body.body-sm, body.body-xs { app-home { diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss deleted file mode 100644 index e1e386d62d..0000000000 --- a/apps/browser/src/popup/scss/plugins.scss +++ /dev/null @@ -1,98 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; -@import "buttons.scss"; - -// Toaster - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 0d7e428138..850ef96c64 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -8,7 +8,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "pages.scss"; @import "@angular/cdk/overlay-prebuilt.css"; diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 4036ace31f..63ce45c9b7 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -2,6 +2,7 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -9,13 +10,13 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { BrowserApi } from "../../platform/browser/browser-api"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; - @Injectable() export class InitService { constructor( private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, private stateService: StateServiceAbstraction, + private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, @Inject(DOCUMENT) private document: Document, @@ -23,8 +24,9 @@ export class InitService { init() { return async () => { - await this.stateService.init(); + await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.i18nService.init(); + this.twoFactorService.init(); if (!BrowserPopupUtils.inPopup(window)) { window.document.body.classList.add("body-full"); diff --git a/apps/browser/src/popup/services/popup-search.service.ts b/apps/browser/src/popup/services/popup-search.service.ts deleted file mode 100644 index 40e6fd2d96..0000000000 --- a/apps/browser/src/popup/services/popup-search.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateProvider } from "@bitwarden/common/platform/state"; -import { SearchService } from "@bitwarden/common/services/search.service"; - -export class PopupSearchService extends SearchService { - constructor(logService: LogService, i18nService: I18nService, stateProvider: StateProvider) { - super(logService, i18nService, stateProvider); - } - - clearIndex(): Promise { - throw new Error("Not available."); - } - - indexCiphers(): Promise { - throw new Error("Not available."); - } - - async getIndexForSearch() { - return await super.getIndexForSearch(); - } -} diff --git a/apps/browser/src/popup/services/popup-utils.service.ts b/apps/browser/src/popup/services/popup-utils.service.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index fe70058640..7dc79fa01e 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,7 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { ToastrService } from "ngx-toastr"; +import { Subject, merge } from "rxjs"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -13,27 +12,26 @@ import { OBSERVABLE_MEMORY_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, + INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { - AuthRequestServiceAbstraction, - LoginStrategyServiceAbstraction, -} from "@bitwarden/auth/common"; +import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { @@ -49,26 +47,31 @@ import { UserNotificationSettingsServiceAbstraction, } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; +import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; 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 { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; +import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { DerivedStateProvider, @@ -83,7 +86,7 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; @@ -91,18 +94,27 @@ import AutofillService from "../../autofill/services/autofill.service"; import MainBackground from "../../background/main.background"; import { Account } from "../../models/account"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { runInsideAngular } from "../../platform/browser/run-inside-angular.operator"; +/* eslint-disable no-restricted-imports */ +import { ChromeMessageSender } from "../../platform/messaging/chrome-message.sender"; +/* eslint-enable no-restricted-imports */ +import { OffscreenDocumentService } from "../../platform/offscreen-document/abstractions/offscreen-document"; +import { DefaultOffscreenDocumentService } from "../../platform/offscreen-document/offscreen-document.service"; import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; import { BrowserFileDownloadService } from "../../platform/popup/services/browser-file-download.service"; import { BrowserStateService as StateServiceAbstraction } from "../../platform/services/abstractions/browser-state.service"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; +import { BrowserCryptoService } from "../../platform/services/browser-crypto.service"; import { BrowserEnvironmentService } from "../../platform/services/browser-environment.service"; import BrowserLocalStorageService from "../../platform/services/browser-local-storage.service"; -import BrowserMessagingPrivateModePopupService from "../../platform/services/browser-messaging-private-mode-popup.service"; -import BrowserMessagingService from "../../platform/services/browser-messaging.service"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import I18nService from "../../platform/services/i18n.service"; import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service"; import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider"; +import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; +import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; import { VaultBrowserStateService } from "../../vault/services/vault-browser-state.service"; @@ -111,16 +123,18 @@ import { VaultFilterService } from "../../vault/services/vault-filter.service"; import { DebounceNavigationService } from "./debounce-navigation.service"; import { InitService } from "./init.service"; import { PopupCloseWarningService } from "./popup-close-warning.service"; -import { PopupSearchService } from "./popup-search.service"; + +const OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE = new SafeInjectionToken< + AbstractStorageService & ObservableStorageService +>("OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE"); const needsBackgroundInit = BrowserPopupUtils.backgroundInitializationRequired(); -const isPrivateMode = BrowserPopupUtils.inPrivateMode(); const mainBackground: MainBackground = needsBackgroundInit ? createLocalBgService() : BrowserApi.getBackgroundPage().bitwardenMain; function createLocalBgService() { - const localBgService = new MainBackground(isPrivateMode, true); + const localBgService = new MainBackground(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 localBgService.bootstrap(); @@ -155,55 +169,21 @@ const safeProviders: SafeProvider[] = [ useClass: UnauthGuardService, deps: [AuthServiceAbstraction, Router], }), - safeProvider({ - provide: MessagingService, - useFactory: () => { - return needsBackgroundInit && BrowserApi.isManifestVersion(2) - ? new BrowserMessagingPrivateModePopupService() - : new BrowserMessagingService(); - }, - deps: [], - }), - safeProvider({ - provide: TwoFactorService, - useFactory: getBgService("twoFactorService"), - deps: [], - }), safeProvider({ provide: AuthServiceAbstraction, useFactory: getBgService("authService"), deps: [], }), - safeProvider({ - provide: LoginStrategyServiceAbstraction, - useFactory: getBgService("loginStrategyService"), - deps: [], - }), safeProvider({ provide: SsoLoginServiceAbstraction, useFactory: getBgService("ssoLoginService"), deps: [], }), - safeProvider({ - provide: SearchServiceAbstraction, - useClass: PopupSearchService, - deps: [LogService, I18nServiceAbstraction, StateProvider], - }), - safeProvider({ - provide: CipherService, - useFactory: getBgService("cipherService"), - deps: [], - }), safeProvider({ provide: CryptoFunctionService, useFactory: () => new WebCryptoFunctionService(window), deps: [], }), - safeProvider({ - provide: CollectionService, - useFactory: getBgService("collectionService"), - deps: [], - }), safeProvider({ provide: LogService, useFactory: (platformUtilsService: PlatformUtilsService) => @@ -228,12 +208,48 @@ const safeProviders: SafeProvider[] = [ }), safeProvider({ provide: CryptoService, - useFactory: (encryptService: EncryptService) => { - const cryptoService = getBgService("cryptoService")(); + useFactory: ( + masterPasswordService: InternalMasterPasswordServiceAbstraction, + keyGenerationService: KeyGenerationService, + cryptoFunctionService: CryptoFunctionService, + encryptService: EncryptService, + platformUtilsService: PlatformUtilsService, + logService: LogService, + stateService: StateServiceAbstraction, + accountService: AccountServiceAbstraction, + stateProvider: StateProvider, + biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, + ) => { + const cryptoService = new BrowserCryptoService( + masterPasswordService, + keyGenerationService, + cryptoFunctionService, + encryptService, + platformUtilsService, + logService, + stateService, + accountService, + stateProvider, + biometricStateService, + kdfConfigService, + ); new ContainerService(cryptoService, encryptService).attachToGlobal(self); return cryptoService; }, - deps: [EncryptService], + deps: [ + InternalMasterPasswordServiceAbstraction, + KeyGenerationService, + CryptoFunctionService, + EncryptService, + PlatformUtilsService, + LogService, + StateServiceAbstraction, + AccountServiceAbstraction, + StateProvider, + BiometricStateService, + KdfConfigService, + ], }), safeProvider({ provide: TotpServiceAbstraction, @@ -246,8 +262,8 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useFactory: getBgService("deviceTrustCryptoService"), + provide: DeviceTrustServiceAbstraction, + useFactory: getBgService("deviceTrustService"), deps: [], }), safeProvider({ @@ -256,16 +272,18 @@ const safeProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: PlatformUtilsService, - useExisting: ForegroundPlatformUtilsService, + provide: OffscreenDocumentService, + useClass: DefaultOffscreenDocumentService, + deps: [], }), safeProvider({ - provide: ForegroundPlatformUtilsService, - useClass: ForegroundPlatformUtilsService, - useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => { + provide: PlatformUtilsService, + useFactory: ( + toastService: ToastService, + offscreenDocumentService: OffscreenDocumentService, + ) => { return new ForegroundPlatformUtilsService( - sanitizer, - toastrService, + toastService, (clipboardValue: string, clearMs: number) => { void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs }); }, @@ -280,9 +298,10 @@ const safeProviders: SafeProvider[] = [ return response.result; }, window, + offscreenDocumentService, ); }, - deps: [DomSanitizer, ToastrService], + deps: [ToastService, OffscreenDocumentService], }), safeProvider({ provide: PasswordGenerationServiceAbstraction, @@ -319,8 +338,15 @@ const safeProviders: SafeProvider[] = [ DomainSettingsService, UserVerificationService, BillingAccountProfileStateService, + ScriptInjectorService, + AccountServiceAbstraction, ], }), + safeProvider({ + provide: ScriptInjectorService, + useClass: BrowserScriptInjectorService, + deps: [PlatformUtilsService, LogService], + }), safeProvider({ provide: KeyConnectorService, useFactory: getBgService("keyConnectorService"), @@ -381,6 +407,21 @@ const safeProviders: SafeProvider[] = [ }, deps: [], }), + safeProvider({ + provide: OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + useFactory: ( + regularMemoryStorageService: AbstractStorageService & ObservableStorageService, + ) => { + if (BrowserApi.isManifestVersion(2)) { + return regularMemoryStorageService; + } + + return getBgService( + "largeObjectMemoryStorageForStateProviders", + )(); + }, + deps: [OBSERVABLE_MEMORY_STORAGE], + }), safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService, @@ -397,7 +438,7 @@ const safeProviders: SafeProvider[] = [ useFactory: ( storageService: AbstractStorageService, secureStorageService: AbstractStorageService, - memoryStorageService: AbstractMemoryStorageService, + memoryStorageService: AbstractStorageService, logService: LogService, accountService: AccountServiceAbstraction, environmentService: EnvironmentService, @@ -467,7 +508,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: ForegroundDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE, NgZone], + deps: [NgZone], }), safeProvider({ provide: AutofillSettingsServiceAbstraction, @@ -484,6 +525,78 @@ const safeProviders: SafeProvider[] = [ useClass: BrowserSendStateService, deps: [StateProvider], }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>, ngZone: NgZone) => + new MessageListener( + merge( + subject.asObservable(), // For messages in the same context + fromChromeRuntimeMessaging().pipe(runInsideAngular(ngZone)), // For messages in the same context + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, NgZone], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (BrowserPopupUtils.backgroundInitializationRequired()) { + // There is no persistent main background which means we have one in memory, + // we need the same instance that our in memory background is utilizing. + return getBgService("intraprocessMessagingSubject")(); + } else { + return new Subject>(); + } + }, + deps: [], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>, logService: LogService) => + MessageSender.combine( + new SubjectMessageSender(subject), // For sending messages in the same context + new ChromeMessageSender(logService), // For sending messages to different contexts + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT, LogService], + }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => { + if (needsBackgroundInit) { + // We will have created a popup within this context, in that case + // we want to make sure we have the same subject as that context so we + // can message with it. + return getBgService("intraprocessMessagingSubject")(); + } else { + // There isn't a locally created background so we will communicate with + // the true background through chrome apis, in that case, we can just create + // one for ourself. + return new Subject>(); + } + }, + deps: [], + }), + safeProvider({ + provide: StorageServiceProvider, + useClass: BrowserStorageServiceProvider, + deps: [ + OBSERVABLE_DISK_STORAGE, + OBSERVABLE_MEMORY_STORAGE, + OBSERVABLE_LARGE_OBJECT_MEMORY_STORAGE, + ], + }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Browser, + }), ]; @NgModule({ diff --git a/apps/browser/src/popup/settings/settings.component.html b/apps/browser/src/popup/settings/settings.component.html deleted file mode 100644 index 98c218b0db..0000000000 --- a/apps/browser/src/popup/settings/settings.component.html +++ /dev/null @@ -1,264 +0,0 @@ - -
- -
-

- {{ "settings" | i18n }} -

-
-
-
-
-

{{ "manage" | i18n }}

-
- - - - -
-
-
-

{{ "security" | i18n }}

-
- - - {{ - "vaultTimeoutPolicyWithActionInEffect" - | i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n) - }} - - - {{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }} - - - {{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }} - - - - -
- - -
- -
- - -
-
- - -
-
- - -
- - -
-
-
-

{{ "account" | i18n }}

-
- - - - -
-
-
-

{{ "tools" | i18n }}

-
- - - -
-
-
-

{{ "other" | i18n }}

-
- - - - - -
- -
-
diff --git a/apps/browser/src/popup/tabs-v2.component.ts b/apps/browser/src/popup/tabs-v2.component.ts new file mode 100644 index 0000000000..4cdb8fc029 --- /dev/null +++ b/apps/browser/src/popup/tabs-v2.component.ts @@ -0,0 +1,11 @@ +import { Component } from "@angular/core"; + +@Component({ + selector: "app-tabs-v2", + template: ` + + + + `, +}) +export class TabsV2Component {} diff --git a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts index e4b8413718..2ade5bf767 100644 --- a/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/abstractions/fileless-importer.background.ts @@ -1,10 +1,5 @@ import { FilelessImportTypeKeys } from "../../enums/fileless-import.enums"; -type SuppressDownloadScriptInjectionConfig = { - file: string; - scriptingApiDetails?: { world: chrome.scripting.ExecutionWorld }; -}; - type FilelessImportPortMessage = { command?: string; importType?: FilelessImportTypeKeys; @@ -32,7 +27,6 @@ interface FilelessImporterBackground { } export { - SuppressDownloadScriptInjectionConfig, FilelessImportPortMessage, ImportNotificationMessageHandlers, LpImporterMessageHandlers, 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 858889b887..7b356b18fd 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.spec.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.spec.ts @@ -5,6 +5,8 @@ import { PolicyService } from "@bitwarden/common/admin-console/services/policy/p import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { Importer, ImportResult, ImportServiceAbstraction } from "@bitwarden/importer/core"; @@ -16,6 +18,7 @@ import { triggerRuntimeOnConnectEvent, } from "../../autofill/spec/testing-utils"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../platform/services/browser-script-injector.service"; import { FilelessImportPort, FilelessImportType } from "../enums/fileless-import.enums"; import FilelessImporterBackground from "./fileless-importer.background"; @@ -37,8 +40,12 @@ describe("FilelessImporterBackground ", () => { const notificationBackground = mock(); const importService = mock(); const syncService = mock(); + const platformUtilsService = mock(); + const logService = mock(); + let scriptInjectorService: BrowserScriptInjectorService; beforeEach(() => { + scriptInjectorService = new BrowserScriptInjectorService(platformUtilsService, logService); filelessImporterBackground = new FilelessImporterBackground( configService, authService, @@ -46,6 +53,7 @@ describe("FilelessImporterBackground ", () => { notificationBackground, importService, syncService, + scriptInjectorService, ); filelessImporterBackground.init(); }); @@ -138,7 +146,7 @@ describe("FilelessImporterBackground ", () => { expect(executeScriptInTabSpy).toHaveBeenCalledWith( lpImporterPort.sender.tab.id, - { file: "content/lp-suppress-import-download.js", runAt: "document_start" }, + { file: "content/lp-suppress-import-download.js", runAt: "document_start", frameId: 0 }, { world: "MAIN" }, ); }); @@ -149,14 +157,11 @@ describe("FilelessImporterBackground ", () => { triggerRuntimeOnConnectEvent(lpImporterPort); await flushPromises(); - expect(executeScriptInTabSpy).toHaveBeenCalledWith( - lpImporterPort.sender.tab.id, - { - file: "content/lp-suppress-import-download-script-append-mv2.js", - runAt: "document_start", - }, - undefined, - ); + expect(executeScriptInTabSpy).toHaveBeenCalledWith(lpImporterPort.sender.tab.id, { + file: "content/lp-suppress-import-download-script-append-mv2.js", + runAt: "document_start", + frameId: 0, + }); }); }); diff --git a/apps/browser/src/tools/background/fileless-importer.background.ts b/apps/browser/src/tools/background/fileless-importer.background.ts index 57c2faa930..fed5541f52 100644 --- a/apps/browser/src/tools/background/fileless-importer.background.ts +++ b/apps/browser/src/tools/background/fileless-importer.background.ts @@ -11,6 +11,7 @@ import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import NotificationBackground from "../../autofill/background/notification.background"; import { BrowserApi } from "../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../platform/services/abstractions/script-injector.service"; import { FilelessImporterInjectedScriptsConfig } from "../config/fileless-importer-injected-scripts"; import { FilelessImportPort, @@ -23,7 +24,6 @@ import { LpImporterMessageHandlers, FilelessImporterBackground as FilelessImporterBackgroundInterface, FilelessImportPortMessage, - SuppressDownloadScriptInjectionConfig, } from "./abstractions/fileless-importer.background"; class FilelessImporterBackground implements FilelessImporterBackgroundInterface { @@ -53,6 +53,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface * @param notificationBackground - Used to inject the notification bar into the tab. * @param importService - Used to import the export data into the vault. * @param syncService - Used to trigger a full sync after the import is completed. + * @param scriptInjectorService - Used to inject content scripts that initialize the import process */ constructor( private configService: ConfigService, @@ -61,6 +62,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface private notificationBackground: NotificationBackground, private importService: ImportServiceAbstraction, private syncService: SyncService, + private scriptInjectorService: ScriptInjectorService, ) {} /** @@ -110,23 +112,6 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface await this.notificationBackground.requestFilelessImport(tab, importType); } - /** - * Injects the script used to suppress the download of the LP importer export file. - * - * @param sender - The sender of the message. - * @param injectionConfig - The configuration for the injection. - */ - private async injectScriptConfig( - sender: chrome.runtime.MessageSender, - injectionConfig: SuppressDownloadScriptInjectionConfig, - ) { - await BrowserApi.executeScriptInTab( - sender.tab.id, - { file: injectionConfig.file, runAt: "document_start" }, - injectionConfig.scriptingApiDetails, - ); - } - /** * Triggers the download of the CSV file from the LP importer. This is triggered * when the user opts to not save the export to Bitwarden within the notification bar. @@ -198,7 +183,7 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface return; } - const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( + const filelessImportFeatureFlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.BrowserFilelessImport, ); const userAuthStatus = await this.authService.getAuthStatus(); @@ -219,12 +204,12 @@ class FilelessImporterBackground implements FilelessImporterBackgroundInterface switch (port.name) { case FilelessImportPort.LpImporter: this.lpImporterPort = port; - await this.injectScriptConfig( - port.sender, - BrowserApi.manifestVersion === 3 - ? FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3 - : FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, - ); + await this.scriptInjectorService.inject({ + tabId: port.sender.tab.id, + injectDetails: { runAt: "document_start" }, + mv2Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv2, + mv3Details: FilelessImporterInjectedScriptsConfig.LpSuppressImportDownload.mv3, + }); break; case FilelessImportPort.NotificationBar: this.importNotificationsPort = port; diff --git a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts index dbc05fe18c..898ee1205a 100644 --- a/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts +++ b/apps/browser/src/tools/config/fileless-importer-injected-scripts.ts @@ -1,9 +1,12 @@ -import { SuppressDownloadScriptInjectionConfig } from "../background/abstractions/fileless-importer.background"; +import { + Mv2ScriptInjectionDetails, + Mv3ScriptInjectionDetails, +} from "../../platform/services/abstractions/script-injector.service"; type FilelessImporterInjectedScriptsConfigurations = { LpSuppressImportDownload: { - mv2: SuppressDownloadScriptInjectionConfig; - mv3: SuppressDownloadScriptInjectionConfig; + mv2: Mv2ScriptInjectionDetails; + mv3: Mv3ScriptInjectionDetails; }; }; @@ -14,7 +17,7 @@ const FilelessImporterInjectedScriptsConfig: FilelessImporterInjectedScriptsConf }, mv3: { file: "content/lp-suppress-import-download.js", - scriptingApiDetails: { world: "MAIN" }, + world: "MAIN", }, }, } as const; diff --git a/apps/browser/src/tools/popup/send/send-add-edit.component.ts b/apps/browser/src/tools/popup/send/send-add-edit.component.ts index baf985b6e9..c20bf7cb8d 100644 --- a/apps/browser/src/tools/popup/send/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send/send-add-edit.component.ts @@ -6,6 +6,7 @@ import { first } from "rxjs/operators"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -51,6 +52,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { formBuilder: FormBuilder, private filePopoutUtilsService: FilePopoutUtilsService, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -66,6 +68,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); } diff --git a/apps/browser/src/tools/popup/services/browser-send-state.service.ts b/apps/browser/src/tools/popup/services/browser-send-state.service.ts index 52aeb01a92..11e71c9b20 100644 --- a/apps/browser/src/tools/popup/services/browser-send-state.service.ts +++ b/apps/browser/src/tools/popup/services/browser-send-state.service.ts @@ -46,7 +46,9 @@ export class BrowserSendStateService { * the send component on the browser */ async setBrowserSendComponentState(value: BrowserSendComponentState): Promise { - await this.activeUserBrowserSendComponentState.update(() => value); + await this.activeUserBrowserSendComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } /** Get the active user's browser component state @@ -60,6 +62,8 @@ export class BrowserSendStateService { * @param { BrowserComponentState } value set the scroll position and search text for the send component on the browser */ async setBrowserSendTypeComponentState(value: BrowserComponentState): Promise { - await this.activeUserBrowserSendTypeComponentState.update(() => value); + await this.activeUserBrowserSendTypeComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/tools/popup/settings/about/about.component.html similarity index 96% rename from apps/browser/src/popup/settings/about.component.html rename to apps/browser/src/tools/popup/settings/about/about.component.html index a4ad0ba801..e68a664ba7 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/tools/popup/settings/about/about.component.html @@ -5,7 +5,7 @@
Bitwarden

© Bitwarden Inc. 2015-{{ year }}

-

{{ "version" | i18n }}: {{ version }}

+

{{ "version" | i18n }}: {{ version$ | async }}

{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} diff --git a/apps/browser/src/popup/settings/about.component.ts b/apps/browser/src/tools/popup/settings/about/about.component.ts similarity index 73% rename from apps/browser/src/popup/settings/about.component.ts rename to apps/browser/src/tools/popup/settings/about/about.component.ts index 61b5749b51..d7f98c1e7f 100644 --- a/apps/browser/src/popup/settings/about.component.ts +++ b/apps/browser/src/tools/popup/settings/about/about.component.ts @@ -1,14 +1,13 @@ import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; -import { combineLatest, map } from "rxjs"; +import { Observable, combineLatest, defer, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule, DialogModule } from "@bitwarden/components"; -import { BrowserApi } from "../../platform/browser/browser-api"; - @Component({ templateUrl: "about.component.html", standalone: true, @@ -16,7 +15,7 @@ import { BrowserApi } from "../../platform/browser/browser-api"; }) export class AboutComponent { protected year = new Date().getFullYear(); - protected version = BrowserApi.getApplicationVersion(); + protected version$: Observable; protected data$ = combineLatest([ this.configService.serverConfig$, @@ -26,5 +25,8 @@ export class AboutComponent { constructor( private configService: ConfigService, private environmentService: EnvironmentService, - ) {} + private platformUtilsService: PlatformUtilsService, + ) { + this.version$ = defer(() => this.platformUtilsService.getApplicationVersion()); + } } diff --git a/apps/browser/src/tools/popup/settings/export.component.html b/apps/browser/src/tools/popup/settings/export.component.html index aae3584f6c..1b2ea1eb1d 100644 --- a/apps/browser/src/tools/popup/settings/export.component.html +++ b/apps/browser/src/tools/popup/settings/export.component.html @@ -1,7 +1,7 @@

- diff --git a/apps/browser/src/tools/popup/settings/import/import-browser.component.html b/apps/browser/src/tools/popup/settings/import/import-browser.component.html index df4f3f09aa..67b5eb348a 100644 --- a/apps/browser/src/tools/popup/settings/import/import-browser.component.html +++ b/apps/browser/src/tools/popup/settings/import/import-browser.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/tools/popup/settings/settings.component.html b/apps/browser/src/tools/popup/settings/settings.component.html new file mode 100644 index 0000000000..71f4f1b991 --- /dev/null +++ b/apps/browser/src/tools/popup/settings/settings.component.html @@ -0,0 +1,128 @@ + +
+ +
+

+ {{ "settings" | i18n }} +

+
+
+
+
+

{{ "manage" | i18n }}

+
+ + + + +
+
+
+

{{ "account" | i18n }}

+
+ +
+
+
+

{{ "tools" | i18n }}

+
+ +
+
+
+

{{ "other" | i18n }}

+
+ + + + + +
+ +
+
diff --git a/apps/browser/src/tools/popup/settings/settings.component.ts b/apps/browser/src/tools/popup/settings/settings.component.ts new file mode 100644 index 0000000000..81727c442c --- /dev/null +++ b/apps/browser/src/tools/popup/settings/settings.component.ts @@ -0,0 +1,101 @@ +import { Component, OnInit } from "@angular/core"; +import { Router } from "@angular/router"; +import { firstValueFrom, Subject } from "rxjs"; + +import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { DialogService } from "@bitwarden/components"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +import { AboutComponent } from "./about/about.component"; + +const RateUrls = { + [DeviceType.ChromeExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.FirefoxExtension]: + "https://addons.mozilla.org/en-US/firefox/addon/bitwarden-password-manager/#reviews", + [DeviceType.OperaExtension]: + "https://addons.opera.com/en/extensions/details/bitwarden-free-password-manager/#feedback-container", + [DeviceType.EdgeExtension]: + "https://microsoftedge.microsoft.com/addons/detail/jbkfoedolllekgbhcbcoahefnbanhhlh", + [DeviceType.VivaldiExtension]: + "https://chromewebstore.google.com/detail/bitwarden-free-password-m/nngceckbapebfimnlniiiahkandclblb/reviews", + [DeviceType.SafariExtension]: "https://apps.apple.com/app/bitwarden/id1352778147", +}; + +@Component({ + selector: "tools-settings", + templateUrl: "settings.component.html", +}) +// eslint-disable-next-line rxjs-angular/prefer-takeuntil +export class SettingsComponent implements OnInit { + private destroy$ = new Subject(); + + constructor( + private platformUtilsService: PlatformUtilsService, + private i18nService: I18nService, + private vaultTimeoutService: VaultTimeoutService, + public messagingService: MessagingService, + private router: Router, + private environmentService: EnvironmentService, + private dialogService: DialogService, + ) {} + + async ngOnInit() {} + + async share() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "learnOrg" }, + content: { key: "learnOrgConfirmation" }, + type: "info", + }); + if (confirmed) { + // 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/about-organizations/"); + } + } + + async webVault() { + const env = await firstValueFrom(this.environmentService.environment$); + const url = env.getWebVaultUrl(); + await BrowserApi.createNewTab(url); + } + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + // 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 + BrowserPopupUtils.openCurrentPagePopout(window); + } + } + + export() { + // 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(["/export"]); + } + + about() { + this.dialogService.open(AboutComponent); + } + + rate() { + const deviceType = this.platformUtilsService.getDevice(); + // 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((RateUrls as any)[deviceType]); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } +} diff --git a/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts new file mode 100644 index 0000000000..49f248c7b8 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/abstractions/fido2.background.ts @@ -0,0 +1,57 @@ +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +type SharedFido2ScriptInjectionDetails = { + runAt: browser.contentScripts.RegisteredContentScriptOptions["runAt"]; +}; + +type SharedFido2ScriptRegistrationOptions = SharedFido2ScriptInjectionDetails & { + matches: string[]; + excludeMatches: string[]; + allFrames: true; +}; + +type Fido2ExtensionMessage = { + [key: string]: any; + command: string; + hostname?: string; + origin?: string; + requestId?: string; + abortedRequestId?: string; + data?: AssertCredentialParams | CreateCredentialParams; +}; + +type Fido2ExtensionMessageEventParams = { + message: Fido2ExtensionMessage; + sender: chrome.runtime.MessageSender; +}; + +type Fido2BackgroundExtensionMessageHandlers = { + [key: string]: CallableFunction; + fido2AbortRequest: ({ message }: Fido2ExtensionMessageEventParams) => void; + fido2RegisterCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise; + fido2GetCredentialRequest: ({ + message, + sender, + }: Fido2ExtensionMessageEventParams) => Promise; +}; + +interface Fido2Background { + init(): void; + injectFido2ContentScriptsInAllTabs(): Promise; +} + +export { + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, + Fido2ExtensionMessage, + Fido2BackgroundExtensionMessageHandlers, + Fido2Background, +}; diff --git a/apps/browser/src/vault/fido2/background/fido2.background.spec.ts b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts new file mode 100644 index 0000000000..534d8a99c5 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.spec.ts @@ -0,0 +1,414 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + CreateCredentialParams, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; +import { Fido2ClientService } from "@bitwarden/common/vault/services/fido2/fido2-client.service"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendExtensionRuntimeMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { BrowserScriptInjectorService } from "../../../platform/services/browser-script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { Fido2ExtensionMessage } from "./abstractions/fido2.background"; +import { Fido2Background } from "./fido2.background"; + +const sharedExecuteScriptOptions = { runAt: "document_start" }; +const sharedScriptInjectionDetails = { frame: "all_frames", ...sharedExecuteScriptOptions }; +const contentScriptDetails = { + file: Fido2ContentScript.ContentScript, + ...sharedScriptInjectionDetails, +}; +const sharedRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...sharedExecuteScriptOptions, +}; + +describe("Fido2Background", () => { + const tabsQuerySpy: jest.SpyInstance = jest.spyOn(BrowserApi, "tabsQuery"); + const isManifestVersionSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "isManifestVersion"); + const focusTabSpy: jest.SpyInstance = jest.spyOn(BrowserApi, "focusTab").mockResolvedValue(); + const focusWindowSpy: jest.SpyInstance = jest + .spyOn(BrowserApi, "focusWindow") + .mockResolvedValue(); + let abortManagerMock!: MockProxy; + let abortController!: MockProxy; + let registeredContentScripsMock!: MockProxy; + let tabMock!: MockProxy; + let senderMock!: MockProxy; + let logService!: MockProxy; + let fido2ClientService!: MockProxy; + let vaultSettingsService!: MockProxy; + let scriptInjectorServiceMock!: MockProxy; + let enablePasskeysMock$!: BehaviorSubject; + let fido2Background!: Fido2Background; + + beforeEach(() => { + tabMock = mock({ + id: 123, + url: "https://example.com", + windowId: 456, + }); + senderMock = mock({ id: "1", tab: tabMock }); + logService = mock(); + fido2ClientService = mock(); + vaultSettingsService = mock(); + abortManagerMock = mock(); + abortController = mock(); + registeredContentScripsMock = mock(); + scriptInjectorServiceMock = mock(); + + enablePasskeysMock$ = new BehaviorSubject(true); + vaultSettingsService.enablePasskeys$ = enablePasskeysMock$; + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + fido2Background = new Fido2Background( + logService, + fido2ClientService, + vaultSettingsService, + scriptInjectorServiceMock, + ); + fido2Background["abortManager"] = abortManagerMock; + abortManagerMock.runWithAbortController.mockImplementation((_requestId, runner) => + runner(abortController), + ); + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + }); + + afterEach(() => { + jest.resetModules(); + jest.clearAllMocks(); + }); + + describe("injectFido2ContentScriptsInAllTabs", () => { + it("does not inject any FIDO2 content scripts when no tabs have a secure url protocol", async () => { + const insecureTab = mock({ id: 789, url: "http://example.com" }); + tabsQuerySpy.mockResolvedValueOnce([insecureTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("only injects the FIDO2 content script into tabs that contain a secure url protocol", async () => { + const secondTabMock = mock({ id: 456, url: "https://example.com" }); + const insecureTab = mock({ id: 789, url: "http://example.com" }); + const noUrlTab = mock({ id: 101, url: undefined }); + tabsQuerySpy.mockResolvedValueOnce([tabMock, secondTabMock, insecureTab, noUrlTab]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: secondTabMock.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: insecureTab.id, + injectDetails: contentScriptDetails, + }); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalledWith({ + tabId: noUrlTab.id, + injectDetails: contentScriptDetails, + }); + }); + + it("injects the `page-script.js` content script into the provided tab", async () => { + tabsQuerySpy.mockResolvedValueOnce([tabMock]); + + await fido2Background.injectFido2ContentScriptsInAllTabs(); + + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + }); + }); + + describe("handleEnablePasskeysUpdate", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + jest.spyOn(BrowserApi, "registerContentScriptsMv2"); + jest.spyOn(BrowserApi, "registerContentScriptsMv3"); + jest.spyOn(BrowserApi, "unregisterContentScriptsMv3"); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + triggerRuntimeOnConnectEvent(createPortSpyMock("some-other-port")); + + tabsQuerySpy.mockResolvedValue([tabMock]); + }); + + it("does not destroy and re-inject the content scripts when triggering `handleEnablePasskeysUpdate` with an undefined currentEnablePasskeysSetting property", async () => { + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys the content scripts but skips re-injecting them when the enablePasskeys setting is set to `false`", async () => { + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + + it("destroys and re-injects the content scripts when the enablePasskeys setting is set to `true`", async () => { + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: sharedScriptInjectionDetails, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + expect(scriptInjectorServiceMock.inject).toHaveBeenCalledWith({ + tabId: tabMock.id, + injectDetails: contentScriptDetails, + }); + }); + + describe("given manifest v2", () => { + it("registers the page-script-append-mv2.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv2).toHaveBeenCalledWith({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...sharedRegistrationOptions, + }); + }); + + it("unregisters any existing registered content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 2); + fido2Background["registeredContentScripts"] = registeredContentScripsMock; + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(registeredContentScripsMock.unregister).toHaveBeenCalled(); + expect(BrowserApi.registerContentScriptsMv2).not.toHaveBeenCalledTimes(2); + }); + }); + + describe("given manifest v3", () => { + it("registers the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `true`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(true); + await flushPromises(); + + expect(BrowserApi.registerContentScriptsMv3).toHaveBeenCalledWith([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...sharedRegistrationOptions, + }, + ]); + expect(BrowserApi.unregisterContentScriptsMv3).not.toHaveBeenCalled(); + }); + + it("unregisters the page-script.js and content-script.js content scripts when the enablePasskeys setting is set to `false`", async () => { + isManifestVersionSpy.mockImplementation((manifestVersion) => manifestVersion === 3); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(BrowserApi.unregisterContentScriptsMv3).toHaveBeenCalledWith({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + expect(BrowserApi.registerContentScriptsMv3).not.toHaveBeenCalledTimes(2); + }); + }); + }); + + describe("extension message handlers", () => { + beforeEach(() => { + fido2Background.init(); + }); + + it("ignores messages that do not have a handler associated with a command within the message", () => { + const message = mock({ command: "nonexistentCommand" }); + + sendExtensionRuntimeMessage(message); + + expect(abortManagerMock.abort).not.toHaveBeenCalled(); + }); + + it("sends a response for rejected promises returned by a handler", async () => { + const message = mock({ command: "fido2RegisterCredentialRequest" }); + const sender = mock(); + const sendResponse = jest.fn(); + fido2ClientService.createCredential.mockRejectedValue(new Error("error")); + + sendExtensionRuntimeMessage(message, sender, sendResponse); + await flushPromises(); + + expect(sendResponse).toHaveBeenCalledWith({ error: { message: "error" } }); + }); + + describe("fido2AbortRequest message", () => { + it("aborts the request associated with the passed abortedRequestId", async () => { + const message = mock({ + command: "fido2AbortRequest", + abortedRequestId: "123", + }); + + sendExtensionRuntimeMessage(message); + await flushPromises(); + + expect(abortManagerMock.abort).toHaveBeenCalledWith(message.abortedRequestId); + }); + }); + + describe("fido2RegisterCredentialRequest message", () => { + it("creates a credential within the Fido2ClientService", async () => { + const message = mock({ + command: "fido2RegisterCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.createCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + + describe("fido2GetCredentialRequest", () => { + it("asserts a credential within the Fido2ClientService", async () => { + const message = mock({ + command: "fido2GetCredentialRequest", + requestId: "123", + data: mock(), + }); + + sendExtensionRuntimeMessage(message, senderMock); + await flushPromises(); + + expect(fido2ClientService.assertCredential).toHaveBeenCalledWith( + message.data, + tabMock, + abortController, + ); + expect(focusTabSpy).toHaveBeenCalledWith(tabMock.id); + expect(focusWindowSpy).toHaveBeenCalledWith(tabMock.windowId); + }); + }); + }); + + describe("handle ports onConnect", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(true); + }); + + it("ignores port connections that do not have the correct port name", async () => { + const port = createPortSpyMock("nonexistentPort"); + + triggerRuntimeOnConnectEvent(port); + await flushPromises(); + + expect(port.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("ignores port connections that do not have a sender url", async () => { + portMock.sender = undefined; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("disconnects the port connection when the Fido2 feature is not enabled", async () => { + fido2ClientService.isFido2FeatureEnabled.mockResolvedValue(false); + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + }); + + it("disconnects the port connection when the url is malformed", async () => { + portMock.sender.url = "malformed-url"; + + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.disconnect).toHaveBeenCalled(); + expect(logService.error).toHaveBeenCalled(); + }); + + it("adds the port to the fido2ContentScriptPortsSet when the Fido2 feature is enabled", async () => { + triggerRuntimeOnConnectEvent(portMock); + await flushPromises(); + + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("handleInjectScriptPortOnDisconnect", () => { + let portMock!: MockProxy; + + beforeEach(() => { + fido2Background.init(); + portMock = createPortSpyMock(Fido2PortName.InjectedScript); + triggerRuntimeOnConnectEvent(portMock); + fido2Background["fido2ContentScriptPortsSet"].add(portMock); + }); + + it("does not destroy or inject the content script when the port has already disconnected before the enablePasskeys setting is set to `false`", async () => { + triggerPortOnDisconnectEvent(portMock); + + enablePasskeysMock$.next(false); + await flushPromises(); + + expect(portMock.disconnect).not.toHaveBeenCalled(); + expect(scriptInjectorServiceMock.inject).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/background/fido2.background.ts b/apps/browser/src/vault/fido2/background/fido2.background.ts new file mode 100644 index 0000000000..5e51e05d77 --- /dev/null +++ b/apps/browser/src/vault/fido2/background/fido2.background.ts @@ -0,0 +1,356 @@ +import { firstValueFrom, startWith } from "rxjs"; +import { pairwise } from "rxjs/operators"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { + AssertCredentialParams, + AssertCredentialResult, + CreateCredentialParams, + CreateCredentialResult, + Fido2ClientService, +} from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; +import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import { ScriptInjectorService } from "../../../platform/services/abstractions/script-injector.service"; +import { AbortManager } from "../../background/abort-manager"; +import { Fido2ContentScript, Fido2ContentScriptId } from "../enums/fido2-content-script.enum"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { + Fido2Background as Fido2BackgroundInterface, + Fido2BackgroundExtensionMessageHandlers, + Fido2ExtensionMessage, + SharedFido2ScriptInjectionDetails, + SharedFido2ScriptRegistrationOptions, +} from "./abstractions/fido2.background"; + +export class Fido2Background implements Fido2BackgroundInterface { + private abortManager = new AbortManager(); + private fido2ContentScriptPortsSet = new Set(); + private registeredContentScripts: browser.contentScripts.RegisteredContentScript; + private readonly sharedInjectionDetails: SharedFido2ScriptInjectionDetails = { + runAt: "document_start", + }; + private readonly sharedRegistrationOptions: SharedFido2ScriptRegistrationOptions = { + matches: ["https://*/*"], + excludeMatches: ["https://*/*.xml*"], + allFrames: true, + ...this.sharedInjectionDetails, + }; + private readonly extensionMessageHandlers: Fido2BackgroundExtensionMessageHandlers = { + fido2AbortRequest: ({ message }) => this.abortRequest(message), + fido2RegisterCredentialRequest: ({ message, sender }) => + this.registerCredentialRequest(message, sender), + fido2GetCredentialRequest: ({ message, sender }) => this.getCredentialRequest(message, sender), + }; + + constructor( + private logService: LogService, + private fido2ClientService: Fido2ClientService, + private vaultSettingsService: VaultSettingsService, + private scriptInjectorService: ScriptInjectorService, + ) {} + + /** + * Initializes the FIDO2 background service. Sets up the extension message + * and port listeners. Subscribes to the enablePasskeys$ observable to + * handle passkey enable/disable events. + */ + init() { + BrowserApi.messageListener("fido2.background", this.handleExtensionMessage); + BrowserApi.addListener(chrome.runtime.onConnect, this.handleInjectedScriptPortConnection); + this.vaultSettingsService.enablePasskeys$ + .pipe(startWith(undefined), pairwise()) + .subscribe(([previous, current]) => this.handleEnablePasskeysUpdate(previous, current)); + } + + /** + * Injects the FIDO2 content and page script into all existing browser tabs. + */ + async injectFido2ContentScriptsInAllTabs() { + const tabs = await BrowserApi.tabsQuery({}); + + for (let index = 0; index < tabs.length; index++) { + const tab = tabs[index]; + + if (tab.url?.startsWith("https")) { + void this.injectFido2ContentScripts(tab); + } + } + } + + /** + * Handles reacting to the enablePasskeys setting being updated. If the setting + * is enabled, the FIDO2 content scripts are injected into all tabs. If the setting + * is disabled, the FIDO2 content scripts will be from all tabs. This logic will + * not trigger until after the first setting update. + * + * @param previousEnablePasskeysSetting - The previous value of the enablePasskeys setting. + * @param enablePasskeys - The new value of the enablePasskeys setting. + */ + private async handleEnablePasskeysUpdate( + previousEnablePasskeysSetting: boolean, + enablePasskeys: boolean, + ) { + await this.updateContentScriptRegistration(); + + if (previousEnablePasskeysSetting === undefined) { + return; + } + + this.destroyLoadedFido2ContentScripts(); + if (enablePasskeys) { + void this.injectFido2ContentScriptsInAllTabs(); + } + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting. + */ + private async updateContentScriptRegistration() { + if (BrowserApi.isManifestVersion(2)) { + await this.updateMv2ContentScriptsRegistration(); + + return; + } + + await this.updateMv3ContentScriptsRegistration(); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v2. + */ + private async updateMv2ContentScriptsRegistration() { + if (!(await this.isPasskeySettingEnabled())) { + await this.registeredContentScripts?.unregister(); + + return; + } + + this.registeredContentScripts = await BrowserApi.registerContentScriptsMv2({ + js: [ + { file: Fido2ContentScript.PageScriptAppend }, + { file: Fido2ContentScript.ContentScript }, + ], + ...this.sharedRegistrationOptions, + }); + } + + /** + * Updates the registration status of static FIDO2 content + * scripts based on the enablePasskeys setting for manifest v3. + */ + private async updateMv3ContentScriptsRegistration() { + if (await this.isPasskeySettingEnabled()) { + void BrowserApi.registerContentScriptsMv3([ + { + id: Fido2ContentScriptId.PageScript, + js: [Fido2ContentScript.PageScript], + world: "MAIN", + ...this.sharedRegistrationOptions, + }, + { + id: Fido2ContentScriptId.ContentScript, + js: [Fido2ContentScript.ContentScript], + ...this.sharedRegistrationOptions, + }, + ]); + + return; + } + + void BrowserApi.unregisterContentScriptsMv3({ + ids: [Fido2ContentScriptId.PageScript, Fido2ContentScriptId.ContentScript], + }); + } + + /** + * Injects the FIDO2 content and page script into the current tab. + * + * @param tab - The current tab to inject the scripts into. + */ + private async injectFido2ContentScripts(tab: chrome.tabs.Tab): Promise { + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { frame: "all_frames", ...this.sharedInjectionDetails }, + mv2Details: { file: Fido2ContentScript.PageScriptAppend }, + mv3Details: { file: Fido2ContentScript.PageScript, world: "MAIN" }, + }); + + void this.scriptInjectorService.inject({ + tabId: tab.id, + injectDetails: { + file: Fido2ContentScript.ContentScript, + frame: "all_frames", + ...this.sharedInjectionDetails, + }, + }); + } + + /** + * Iterates over the set of injected FIDO2 content script ports + * and disconnects them, destroying the content scripts. + */ + private destroyLoadedFido2ContentScripts() { + this.fido2ContentScriptPortsSet.forEach((port) => { + port.disconnect(); + this.fido2ContentScriptPortsSet.delete(port); + }); + } + + /** + * Aborts the FIDO2 request with the provided requestId. + * + * @param message - The FIDO2 extension message containing the requestId to abort. + */ + private abortRequest(message: Fido2ExtensionMessage) { + this.abortManager.abort(message.abortedRequestId); + } + + /** + * Registers a new FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async registerCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise { + return await this.handleCredentialRequest( + message, + sender.tab, + this.fido2ClientService.createCredential.bind(this.fido2ClientService), + ); + } + + /** + * Gets a FIDO2 credential with the provided request data. + * + * @param message - The FIDO2 extension message containing the request data. + * @param sender - The sender of the message. + */ + private async getCredentialRequest( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + ): Promise { + return await this.handleCredentialRequest( + message, + sender.tab, + this.fido2ClientService.assertCredential.bind(this.fido2ClientService), + ); + } + + /** + * Handles Fido2 credential requests by calling the provided callback with the + * request data, tab, and abort controller. The callback is expected to return + * a promise that resolves with the result of the credential request. + * + * @param requestId - The request ID associated with the request. + * @param data - The request data to handle. + * @param tab - The tab associated with the request. + * @param callback - The callback to call with the request data, tab, and abort controller. + */ + private handleCredentialRequest = async ( + { requestId, data }: Fido2ExtensionMessage, + tab: chrome.tabs.Tab, + callback: ( + data: AssertCredentialParams | CreateCredentialParams, + tab: chrome.tabs.Tab, + abortController: AbortController, + ) => Promise, + ) => { + return await this.abortManager.runWithAbortController(requestId, async (abortController) => { + try { + return await callback(data, tab, abortController); + } finally { + await BrowserApi.focusTab(tab.id); + await BrowserApi.focusWindow(tab.windowId); + } + }); + }; + + /** + * Checks if the enablePasskeys setting is enabled. + */ + private async isPasskeySettingEnabled() { + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); + } + + /** + * Handles the FIDO2 extension message by calling the + * appropriate handler based on the message command. + * + * @param message - The FIDO2 extension message to handle. + * @param sender - The sender of the message. + * @param sendResponse - The function to call with the response. + */ + private handleExtensionMessage = ( + message: Fido2ExtensionMessage, + sender: chrome.runtime.MessageSender, + sendResponse: (response?: any) => void, + ) => { + const handler: CallableFunction | undefined = this.extensionMessageHandlers[message?.command]; + if (!handler) { + return; + } + + const messageResponse = handler({ message, sender }); + if (!messageResponse) { + return; + } + + Promise.resolve(messageResponse) + .then( + (response) => sendResponse(response), + (error) => sendResponse({ error: { ...error, message: error.message } }), + ) + .catch(this.logService.error); + + return true; + }; + + /** + * Handles the connection of a FIDO2 content script port by checking if the + * FIDO2 feature is enabled for the sender's hostname and origin. If the feature + * is not enabled, the port is disconnected. + * + * @param port - The port which is connecting + */ + private handleInjectedScriptPortConnection = async (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript || !port.sender?.url) { + return; + } + + try { + const { hostname, origin } = new URL(port.sender.url); + if (!(await this.fido2ClientService.isFido2FeatureEnabled(hostname, origin))) { + port.disconnect(); + return; + } + + this.fido2ContentScriptPortsSet.add(port); + port.onDisconnect.addListener(this.handleInjectScriptPortOnDisconnect); + } catch (error) { + this.logService.error(error); + port.disconnect(); + } + }; + + /** + * Handles the disconnection of a FIDO2 content script port + * by removing it from the set of connected ports. + * + * @param port - The port which is disconnecting + */ + private handleInjectScriptPortOnDisconnect = (port: chrome.runtime.Port) => { + if (port.name !== Fido2PortName.InjectedScript) { + return; + } + + this.fido2ContentScriptPortsSet.delete(port); + }; +} diff --git a/apps/browser/src/vault/fido2/content/content-script.spec.ts b/apps/browser/src/vault/fido2/content/content-script.spec.ts new file mode 100644 index 0000000000..0c2a52ed10 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/content-script.spec.ts @@ -0,0 +1,200 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { CreateCredentialResult } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { triggerPortOnDisconnectEvent } from "../../../autofill/spec/testing-utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; + +import { InsecureCreateCredentialParams, MessageType } from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; + +jest.mock("../../../autofill/utils", () => ({ + sendExtensionMessage: jest.fn((command, options) => { + return chrome.runtime.sendMessage(Object.assign({ command }, options)); + }), +})); + +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + +describe("Fido2 Content Script", () => { + beforeAll(() => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + }); + + afterEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + let messenger: Messenger; + const messengerForDOMCommunicationSpy = jest + .spyOn(Messenger, "forDOMCommunication") + .mockImplementation((context) => { + const windowOrigin = context.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + const portSpy: MockProxy = createPortSpyMock(Fido2PortName.InjectedScript); + chrome.runtime.connect = jest.fn(() => portSpy); + + it("destroys the messenger when the port is disconnected", () => { + require("./content-script"); + + triggerPortOnDisconnectEvent(portSpy); + + expect(messenger.destroy).toHaveBeenCalled(); + }); + + it("handles a FIDO2 credential creation request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const mockResult = { credentialId: "mock" } as CreateCredentialResult; + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue(mockResult); + + require("./content-script"); + + const response = await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2RegisterCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + expect(response).toEqual({ + type: MessageType.CredentialCreationResponse, + result: mockResult, + }); + }); + + it("handles a FIDO2 credential get request message from the window message listener, formats the message and sends the formatted message to the extension background", async () => { + const message = mock({ + type: MessageType.CredentialGetRequest, + data: mock(), + }); + + require("./content-script"); + + await messenger.handler!(message, new AbortController()); + + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2GetCredentialRequest", + data: expect.objectContaining({ + origin: globalThis.location.origin, + sameOriginWithAncestors: true, + }), + requestId: expect.any(String), + }); + }); + + it("removes the abort handler when the FIDO2 request is complete", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "removeEventListener"); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + }); + + it("sends an extension message to abort the FIDO2 request when the abort controller is signaled", async () => { + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + const abortSpy = jest.spyOn(abortController.signal, "addEventListener"); + jest + .spyOn(chrome.runtime, "sendMessage") + .mockImplementationOnce(async (extensionId: string, message: unknown, options: any) => { + abortController.abort(); + }); + + require("./content-script"); + + await messenger.handler!(message, abortController); + + expect(abortSpy).toHaveBeenCalled(); + expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ + command: "fido2AbortRequest", + abortedRequestId: expect.any(String), + }); + }); + + it("rejects credential requests and returns an error result", async () => { + const errorMessage = "Test error"; + const message = mock({ + type: MessageType.CredentialCreationRequest, + data: mock(), + }); + const abortController = new AbortController(); + jest.spyOn(chrome.runtime, "sendMessage").mockResolvedValue({ error: errorMessage }); + + require("./content-script"); + const result = messenger.handler!(message, abortController); + + await expect(result).rejects.toEqual(errorMessage); + }); + + it("skips initializing if the document content type is not 'text/html'", () => { + jest.clearAllMocks(); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + contentType: "application/json", + })); + + require("./content-script"); + + expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); + }); + + it("skips initializing if the document location protocol is not 'https'", () => { + jest.clearAllMocks(); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + location: { + ...mockGlobalThisDocument.location, + href: "http://localhost", + origin: "http://localhost", + protocol: "http:", + }, + })); + + require("./content-script"); + + expect(messengerForDOMCommunicationSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/content-script.ts b/apps/browser/src/vault/fido2/content/content-script.ts index c2fc862f55..fe3aafe9fb 100644 --- a/apps/browser/src/vault/fido2/content/content-script.ts +++ b/apps/browser/src/vault/fido2/content/content-script.ts @@ -3,142 +3,138 @@ import { CreateCredentialParams, } from "@bitwarden/common/vault/abstractions/fido2/fido2-client.service.abstraction"; -import { Message, MessageType } from "./messaging/message"; -import { Messenger } from "./messaging/messenger"; +import { sendExtensionMessage } from "../../../autofill/utils"; +import { Fido2PortName } from "../enums/fido2-port-name.enum"; -function isFido2FeatureEnabled(): Promise { - return new Promise((resolve) => { - chrome.runtime.sendMessage( - { - command: "checkFido2FeatureEnabled", - hostname: window.location.hostname, - origin: window.location.origin, - }, - (response: { result?: boolean }) => resolve(response.result), - ); - }); -} +import { + InsecureAssertCredentialParams, + InsecureCreateCredentialParams, + Message, + MessageType, +} from "./messaging/message"; +import { MessageWithMetadata, Messenger } from "./messaging/messenger"; -function isSameOriginWithAncestors() { - try { - return window.self === window.top; - } catch { - return false; - } -} -const messenger = Messenger.forDOMCommunication(window); - -function injectPageScript() { - // Locate an existing page-script on the page - const existingPageScript = document.getElementById("bw-fido2-page-script"); - - // Inject the page-script if it doesn't exist - if (!existingPageScript) { - const s = document.createElement("script"); - s.src = chrome.runtime.getURL("content/fido2/page-script.js"); - s.id = "bw-fido2-page-script"; - (document.head || document.documentElement).appendChild(s); +(function (globalContext) { + const shouldExecuteContentScript = + globalContext.document.contentType === "text/html" && + globalContext.document.location.protocol === "https:"; + if (!shouldExecuteContentScript) { return; } - // If the page-script already exists, send a reconnect message to the page-script - // 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 - messenger.sendReconnectCommand(); -} + // Initialization logic, set up the messenger and connect a port to the background script. + const messenger = Messenger.forDOMCommunication(globalContext.window); + messenger.handler = handleFido2Message; + const port = chrome.runtime.connect({ name: Fido2PortName.InjectedScript }); + port.onDisconnect.addListener(handlePortOnDisconnect); -function initializeFido2ContentScript() { - injectPageScript(); - - messenger.handler = async (message, abortController) => { + /** + * Handles FIDO2 credential requests and returns the result. + * + * @param message - The message to handle. + * @param abortController - The abort controller used to handle exit conditions from the FIDO2 request. + */ + async function handleFido2Message( + message: MessageWithMetadata, + abortController: AbortController, + ) { const requestId = Date.now().toString(); const abortHandler = () => - chrome.runtime.sendMessage({ - command: "fido2AbortRequest", - abortedRequestId: requestId, - }); + sendExtensionMessage("fido2AbortRequest", { abortedRequestId: requestId }); abortController.signal.addEventListener("abort", abortHandler); - if (message.type === MessageType.CredentialCreationRequest) { - return new Promise((resolve, reject) => { - const data: CreateCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2RegisterCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialCreationResponse, - result: response.result, - }); - }, + try { + if (message.type === MessageType.CredentialCreationRequest) { + return handleCredentialCreationRequestMessage( + requestId, + message.data as InsecureCreateCredentialParams, ); - }); - } + } - if (message.type === MessageType.CredentialGetRequest) { - return new Promise((resolve, reject) => { - const data: AssertCredentialParams = { - ...message.data, - origin: window.location.origin, - sameOriginWithAncestors: isSameOriginWithAncestors(), - }; - - chrome.runtime.sendMessage( - { - command: "fido2GetCredentialRequest", - data, - requestId: requestId, - }, - (response) => { - if (response && response.error !== undefined) { - return reject(response.error); - } - - resolve({ - type: MessageType.CredentialGetResponse, - result: response.result, - }); - }, + if (message.type === MessageType.CredentialGetRequest) { + return handleCredentialGetRequestMessage( + requestId, + message.data as InsecureAssertCredentialParams, ); - }).finally(() => - abortController.signal.removeEventListener("abort", abortHandler), - ) as Promise; + } + } finally { + abortController.signal.removeEventListener("abort", abortHandler); } - - return undefined; - }; -} - -async function run() { - if (!(await isFido2FeatureEnabled())) { - return; } - initializeFido2ContentScript(); + /** + * Handles the credential creation request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialCreationRequestMessage( + requestId: string, + data: InsecureCreateCredentialParams, + ): Promise { + return respondToCredentialRequest( + "fido2RegisterCredentialRequest", + MessageType.CredentialCreationResponse, + requestId, + data, + ); + } - const port = chrome.runtime.connect({ name: "fido2ContentScriptReady" }); - port.onDisconnect.addListener(() => { - // Cleanup the messenger and remove the event listener - // 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 - messenger.destroy(); - }); -} + /** + * Handles the credential get request message and returns the result. + * + * @param requestId - The request ID of the message. + * @param data - Data associated with the credential request. + */ + async function handleCredentialGetRequestMessage( + requestId: string, + data: InsecureAssertCredentialParams, + ): Promise { + return respondToCredentialRequest( + "fido2GetCredentialRequest", + MessageType.CredentialGetResponse, + requestId, + data, + ); + } -// Only run the script if the document is an HTML document -if (document.contentType === "text/html") { - void run(); -} + /** + * Sends a message to the extension to handle the + * credential request and returns the result. + * + * @param command - The command to send to the extension. + * @param type - The type of message, either CredentialCreationResponse or CredentialGetResponse. + * @param requestId - The request ID of the message. + * @param messageData - Data associated with the credential request. + */ + async function respondToCredentialRequest( + command: string, + type: MessageType.CredentialCreationResponse | MessageType.CredentialGetResponse, + requestId: string, + messageData: InsecureCreateCredentialParams | InsecureAssertCredentialParams, + ): Promise { + const data: CreateCredentialParams | AssertCredentialParams = { + ...messageData, + origin: globalContext.location.origin, + sameOriginWithAncestors: globalContext.self === globalContext.top, + }; + + const result = await sendExtensionMessage(command, { data, requestId }); + + if (result && result.error !== undefined) { + return Promise.reject(result.error); + } + + return Promise.resolve({ type, result }); + } + + /** + * Handles the disconnect event of the port. Calls + * to the messenger to destroy and tear down the + * implemented page-script.js logic. + */ + function handlePortOnDisconnect() { + void messenger.destroy(); + } +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts index 0c46ac39aa..5283c60882 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.spec.ts @@ -68,7 +68,7 @@ describe("Messenger", () => { const abortController = new AbortController(); // 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 - messengerA.request(createRequest(), abortController); + messengerA.request(createRequest(), abortController.signal); abortController.abort(); const received = handlerB.receive(); diff --git a/apps/browser/src/vault/fido2/content/messaging/messenger.ts b/apps/browser/src/vault/fido2/content/messaging/messenger.ts index cc29282227..f05c138eab 100644 --- a/apps/browser/src/vault/fido2/content/messaging/messenger.ts +++ b/apps/browser/src/vault/fido2/content/messaging/messenger.ts @@ -47,7 +47,7 @@ export class Messenger { } /** - * The handler that will be called when a message is recieved. The handler should return + * The handler that will be called when a message is received. The handler should return * a promise that resolves to the response message. If the handler throws an error, the * error will be sent back to the sender. */ @@ -65,10 +65,10 @@ export class Messenger { * AbortController signals will be forwarded to the content script. * * @param request data to send to the content script - * @param abortController the abort controller that might be used to abort the request + * @param abortSignal the abort controller that might be used to abort the request * @returns the response from the content script */ - async request(request: Message, abortController?: AbortController): Promise { + async request(request: Message, abortSignal?: AbortSignal): Promise { const requestChannel = new MessageChannel(); const { port1: localPort, port2: remotePort } = requestChannel; @@ -82,7 +82,7 @@ export class Messenger { metadata: { SENDER }, type: MessageType.AbortRequest, }); - abortController?.signal.addEventListener("abort", abortListener); + abortSignal?.addEventListener("abort", abortListener); this.broadcastChannel.postMessage( { ...request, SENDER, senderId: this.messengerId }, @@ -90,7 +90,7 @@ export class Messenger { ); const response = await promise; - abortController?.signal.removeEventListener("abort", abortListener); + abortSignal?.removeEventListener("abort", abortListener); if (response.type === MessageType.ErrorResponse) { const error = new Error(); @@ -113,12 +113,7 @@ export class Messenger { const message = event.data; const port = event.ports?.[0]; - if ( - message?.SENDER !== SENDER || - message.senderId == this.messengerId || - message == null || - port == null - ) { + if (message?.SENDER !== SENDER || message.senderId == this.messengerId || port == null) { return; } @@ -167,10 +162,6 @@ export class Messenger { } } - async sendReconnectCommand() { - await this.request({ type: MessageType.ReconnectRequest }); - } - private async sendDisconnectCommand() { await this.request({ type: MessageType.DisconnectRequest }); } diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts new file mode 100644 index 0000000000..d40a725a1f --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.spec.ts @@ -0,0 +1,69 @@ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +describe("FIDO2 page-script for manifest v2", () => { + let createdScriptElement: HTMLScriptElement; + jest.spyOn(window.document, "createElement"); + + afterEach(() => { + Object.defineProperty(window.document, "contentType", { value: "text/html", writable: true }); + jest.clearAllMocks(); + jest.resetModules(); + }); + + 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 }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).not.toHaveBeenCalled(); + }); + + it("appends the `page-script.js` file to the document head when the contentType is `text/html`", () => { + jest.spyOn(window.document.head, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.head.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.head.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("appends the `page-script.js` file to the document element if the head is not available", () => { + window.document.documentElement.removeChild(window.document.head); + jest.spyOn(window.document.documentElement, "insertBefore").mockImplementation((node) => { + createdScriptElement = node as HTMLScriptElement; + return node; + }); + + require("./page-script-append.mv2"); + + expect(window.document.createElement).toHaveBeenCalledWith("script"); + expect(chrome.runtime.getURL).toHaveBeenCalledWith(Fido2ContentScript.PageScript); + expect(window.document.documentElement.insertBefore).toHaveBeenCalledWith( + expect.any(HTMLScriptElement), + window.document.documentElement.firstChild, + ); + expect(createdScriptElement.src).toBe(`chrome-extension://id/${Fido2ContentScript.PageScript}`); + }); + + it("removes the appended `page-script.js` file after the script has triggered a load event", () => { + createdScriptElement = document.createElement("script"); + jest.spyOn(window.document, "createElement").mockImplementation((element) => { + return createdScriptElement; + }); + + require("./page-script-append.mv2"); + + jest.spyOn(createdScriptElement, "remove"); + createdScriptElement.dispatchEvent(new Event("load")); + + expect(createdScriptElement.remove).toHaveBeenCalled(); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts new file mode 100644 index 0000000000..4e806d2990 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script-append.mv2.ts @@ -0,0 +1,19 @@ +/** + * This script handles injection of the FIDO2 override page script into the document. + * This is required for manifest v2, but will be removed when we migrate fully to manifest v3. + */ +import { Fido2ContentScript } from "../enums/fido2-content-script.enum"; + +(function (globalContext) { + if (globalContext.document.contentType !== "text/html") { + return; + } + + const script = globalContext.document.createElement("script"); + script.src = chrome.runtime.getURL(Fido2ContentScript.PageScript); + script.addEventListener("load", () => script.remove()); + + const scriptInsertionPoint = + globalContext.document.head || globalContext.document.documentElement; + scriptInsertionPoint.insertBefore(script, scriptInsertionPoint.firstChild); +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.ts b/apps/browser/src/vault/fido2/content/page-script.ts index 9adea68307..5b04f7c1dd 100644 --- a/apps/browser/src/vault/fido2/content/page-script.ts +++ b/apps/browser/src/vault/fido2/content/page-script.ts @@ -5,212 +5,234 @@ import { WebauthnUtils } from "../webauthn-utils"; import { MessageType } from "./messaging/message"; import { Messenger } from "./messaging/messenger"; -const BrowserPublicKeyCredential = window.PublicKeyCredential; +(function (globalContext) { + const shouldExecuteContentScript = + globalContext.document.contentType === "text/html" && + globalContext.document.location.protocol === "https:"; -const browserNativeWebauthnSupport = window.PublicKeyCredential != undefined; -let browserNativeWebauthnPlatformAuthenticatorSupport = false; -if (!browserNativeWebauthnSupport) { - // Polyfill webauthn support - try { - // credentials is read-only if supported, use type-casting to force assignment - (navigator as any).credentials = { - async create() { - throw new Error("Webauthn not supported in this browser."); - }, - async get() { - throw new Error("Webauthn not supported in this browser."); - }, - }; - window.PublicKeyCredential = class PolyfillPublicKeyCredential { - static isUserVerifyingPlatformAuthenticatorAvailable() { - return Promise.resolve(true); - } - } as any; - window.AuthenticatorAttestationResponse = - class PolyfillAuthenticatorAttestationResponse {} as any; - } catch { - /* empty */ + if (!shouldExecuteContentScript) { + return; } -} -if (browserNativeWebauthnSupport) { - // 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 - BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then((available) => { - browserNativeWebauthnPlatformAuthenticatorSupport = available; + const BrowserPublicKeyCredential = globalContext.PublicKeyCredential; + const BrowserNavigatorCredentials = navigator.credentials; + const BrowserAuthenticatorAttestationResponse = globalContext.AuthenticatorAttestationResponse; - if (!available) { - // Polyfill platform authenticator support - window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => - Promise.resolve(true); + const browserNativeWebauthnSupport = globalContext.PublicKeyCredential != undefined; + let browserNativeWebauthnPlatformAuthenticatorSupport = false; + if (!browserNativeWebauthnSupport) { + // Polyfill webauthn support + try { + // credentials are read-only if supported, use type-casting to force assignment + (navigator as any).credentials = { + async create() { + throw new Error("Webauthn not supported in this browser."); + }, + async get() { + throw new Error("Webauthn not supported in this browser."); + }, + }; + globalContext.PublicKeyCredential = class PolyfillPublicKeyCredential { + static isUserVerifyingPlatformAuthenticatorAvailable() { + return Promise.resolve(true); + } + } as any; + globalContext.AuthenticatorAttestationResponse = + class PolyfillAuthenticatorAttestationResponse {} as any; + } catch { + /* empty */ } - }); -} + } else { + void BrowserPublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable().then( + (available) => { + browserNativeWebauthnPlatformAuthenticatorSupport = available; -const browserCredentials = { - create: navigator.credentials.create.bind( - navigator.credentials, - ) as typeof navigator.credentials.create, - get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, -}; - -const messenger = ((window as any).messenger = Messenger.forDOMCommunication(window)); - -navigator.credentials.create = createWebAuthnCredential; -navigator.credentials.get = getWebAuthnCredential; - -/** - * Creates a new webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function createWebAuthnCredential( - options?: CredentialCreationOptions, - abortController?: AbortController, -): Promise { - if (!isWebauthnCall(options)) { - return await browserCredentials.create(options); - } - - const fallbackSupported = - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform" && - browserNativeWebauthnPlatformAuthenticatorSupport) || - (options?.publicKey?.authenticatorSelection?.authenticatorAttachment !== "platform" && - browserNativeWebauthnSupport); - try { - const response = await messenger.request( - { - type: MessageType.CredentialCreationRequest, - data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + if (!available) { + // Polyfill platform authenticator support + globalContext.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable = () => + Promise.resolve(true); + } }, - abortController, ); + } - if (response.type !== MessageType.CredentialCreationResponse) { - throw new Error("Something went wrong."); - } + const browserCredentials = { + create: navigator.credentials.create.bind( + navigator.credentials, + ) as typeof navigator.credentials.create, + get: navigator.credentials.get.bind(navigator.credentials) as typeof navigator.credentials.get, + }; - return WebauthnUtils.mapCredentialRegistrationResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + const messenger = Messenger.forDOMCommunication(window); + let waitForFocusTimeout: number | NodeJS.Timeout; + let focusListenerHandler: () => void; + + navigator.credentials.create = createWebAuthnCredential; + navigator.credentials.get = getWebAuthnCredential; + + /** + * Creates a new webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function createWebAuthnCredential( + options?: CredentialCreationOptions, + ): Promise { + if (!isWebauthnCall(options)) { return await browserCredentials.create(options); } - throw error; - } -} + const authenticatorAttachmentIsPlatform = + options?.publicKey?.authenticatorSelection?.authenticatorAttachment === "platform"; -/** - * Retrieves a webauthn credential. - * - * @param options Options for creating new credentials. - * @param abortController Abort controller to abort the request if needed. - * @returns Promise that resolves to the new credential object. - */ -async function getWebAuthnCredential( - options?: CredentialRequestOptions, - abortController?: AbortController, -): Promise { - if (!isWebauthnCall(options)) { - return await browserCredentials.get(options); + const fallbackSupported = + (authenticatorAttachmentIsPlatform && browserNativeWebauthnPlatformAuthenticatorSupport) || + (!authenticatorAttachmentIsPlatform && browserNativeWebauthnSupport); + try { + const response = await messenger.request( + { + type: MessageType.CredentialCreationRequest, + data: WebauthnUtils.mapCredentialCreationOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialCreationResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialRegistrationResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.create(options); + } + + throw error; + } } - const fallbackSupported = browserNativeWebauthnSupport; - - try { - if (options?.mediation && options.mediation !== "optional") { - throw new FallbackRequestedError(); - } - - const response = await messenger.request( - { - type: MessageType.CredentialGetRequest, - data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), - }, - abortController, - ); - - if (response.type !== MessageType.CredentialGetResponse) { - throw new Error("Something went wrong."); - } - - return WebauthnUtils.mapCredentialAssertResult(response.result); - } catch (error) { - if (error && error.fallbackRequested && fallbackSupported) { - await waitForFocus(); + /** + * Retrieves a webauthn credential. + * + * @param options Options for creating new credentials. + * @returns Promise that resolves to the new credential object. + */ + async function getWebAuthnCredential(options?: CredentialRequestOptions): Promise { + if (!isWebauthnCall(options)) { return await browserCredentials.get(options); } - throw error; - } -} + const fallbackSupported = browserNativeWebauthnSupport; -function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { - return options && "publicKey" in options; -} + try { + if (options?.mediation && options.mediation !== "optional") { + throw new FallbackRequestedError(); + } -/** - * Wait for window to be focused. - * Safari doesn't allow scripts to trigger webauthn when window is not focused. - * - * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. - * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. - * @returns Promise that resolves when window is focused, or rejects if timeout is reached. - */ -async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { - try { - if (window.top.document.hasFocus()) { - return; + const response = await messenger.request( + { + type: MessageType.CredentialGetRequest, + data: WebauthnUtils.mapCredentialRequestOptions(options, fallbackSupported), + }, + options?.signal, + ); + + if (response.type !== MessageType.CredentialGetResponse) { + throw new Error("Something went wrong."); + } + + return WebauthnUtils.mapCredentialAssertResult(response.result); + } catch (error) { + if (error && error.fallbackRequested && fallbackSupported) { + await waitForFocus(); + return await browserCredentials.get(options); + } + + throw error; } - } catch { - // Cannot access window.top due to cross-origin frame, fallback to waiting - return await new Promise((resolve) => window.setTimeout(resolve, fallbackWait)); } - let focusListener; - const focusPromise = new Promise((resolve) => { - focusListener = () => resolve(); - window.top.addEventListener("focus", focusListener); - }); - - let timeoutId; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = window.setTimeout( - () => - reject( - new DOMException("The operation either timed out or was not allowed.", "AbortError"), - ), - timeout, - ); - }); - - try { - await Promise.race([focusPromise, timeoutPromise]); - } finally { - window.top.removeEventListener("focus", focusListener); - window.clearTimeout(timeoutId); - } -} - -/** - * Sets up a listener to handle cleanup or reconnection when the extension's - * context changes due to being reloaded or unloaded. - */ -messenger.handler = (message, abortController) => { - const type = message.type; - - // Handle cleanup for disconnect request - if (type === MessageType.DisconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = browserCredentials.create; - navigator.credentials.get = browserCredentials.get; + function isWebauthnCall(options?: CredentialCreationOptions | CredentialRequestOptions) { + return options && "publicKey" in options; } - // Handle reinitialization for reconnect request - if (type === MessageType.ReconnectRequest && browserNativeWebauthnSupport) { - navigator.credentials.create = createWebAuthnCredential; - navigator.credentials.get = getWebAuthnCredential; + /** + * Wait for window to be focused. + * Safari doesn't allow scripts to trigger webauthn when window is not focused. + * + * @param fallbackWait How long to wait when the script is not able to add event listeners to `window.top`. Defaults to 500ms. + * @param timeout Maximum time to wait for focus in milliseconds. Defaults to 5 minutes. + * @returns Promise that resolves when window is focused, or rejects if timeout is reached. + */ + async function waitForFocus(fallbackWait = 500, timeout = 5 * 60 * 1000) { + try { + if (globalContext.top.document.hasFocus()) { + return; + } + } catch { + // Cannot access window.top due to cross-origin frame, fallback to waiting + return await new Promise((resolve) => globalContext.setTimeout(resolve, fallbackWait)); + } + + const focusPromise = new Promise((resolve) => { + focusListenerHandler = () => resolve(); + globalContext.top.addEventListener("focus", focusListenerHandler); + }); + + const timeoutPromise = new Promise((_, reject) => { + waitForFocusTimeout = globalContext.setTimeout( + () => + reject( + new DOMException("The operation either timed out or was not allowed.", "AbortError"), + ), + timeout, + ); + }); + + try { + await Promise.race([focusPromise, timeoutPromise]); + } finally { + clearWaitForFocus(); + } } -}; + + function clearWaitForFocus() { + globalContext.top.removeEventListener("focus", focusListenerHandler); + if (waitForFocusTimeout) { + globalContext.clearTimeout(waitForFocusTimeout); + } + } + + function destroy() { + try { + if (browserNativeWebauthnSupport) { + navigator.credentials.create = browserCredentials.create; + navigator.credentials.get = browserCredentials.get; + } else { + (navigator as any).credentials = BrowserNavigatorCredentials; + globalContext.PublicKeyCredential = BrowserPublicKeyCredential; + globalContext.AuthenticatorAttestationResponse = BrowserAuthenticatorAttestationResponse; + } + + clearWaitForFocus(); + void messenger.destroy(); + } catch (e) { + /** empty */ + } + } + + /** + * Sets up a listener to handle cleanup or reconnection when the extension's + * context changes due to being reloaded or unloaded. + */ + messenger.handler = (message) => { + const type = message.type; + + // Handle cleanup for disconnect request + if (type === MessageType.DisconnectRequest) { + destroy(); + } + }; +})(globalThis); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts new file mode 100644 index 0000000000..c235d53cb0 --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-supported.spec.ts @@ -0,0 +1,178 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, + setupMockedWebAuthnSupport, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((context) => { + const windowOrigin = context.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script with native WebAuthn support", () => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + setupMockedWebAuthnSupport(); + + require("./page-script"); + + afterEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("falls back to the default browser credentials API if an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + try { + await navigator.credentials.create(mockCredentialCreationOptions); + expect("This will fail the test").toBe(true); + } catch { + expect(WebauthnUtils.mapCredentialRegistrationResult).not.toHaveBeenCalled(); + } + }); + + it("creates and returns a WebAuthn credential when the navigator API is called to create credentials", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + true, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("falls back to the default browser credentials API when an error occurs", async () => { + window.top.document.hasFocus = jest.fn().mockReturnValue(true); + messenger.request = jest.fn().mockRejectedValue({ fallbackRequested: true }); + + const returnValue = await navigator.credentials.get(mockCredentialRequestOptions); + + expect(returnValue).toBeDefined(); + expect(WebauthnUtils.mapCredentialAssertResult).not.toHaveBeenCalled(); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + true, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); + + describe("content script execution", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + it("skips initializing if the document content type is not 'text/html'", () => { + jest.spyOn(Messenger, "forDOMCommunication"); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + contentType: "json/application", + })); + + require("./content-script"); + + expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); + }); + + it("skips initializing if the document location protocol is not 'https'", () => { + jest.spyOn(Messenger, "forDOMCommunication"); + + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation(() => ({ + ...mockGlobalThisDocument, + location: { + ...mockGlobalThisDocument.location, + href: "http://localhost", + origin: "http://localhost", + protocol: "http:", + }, + })); + + require("./content-script"); + + expect(Messenger.forDOMCommunication).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts new file mode 100644 index 0000000000..4b1f839a1d --- /dev/null +++ b/apps/browser/src/vault/fido2/content/page-script.webauthn-unsupported.spec.ts @@ -0,0 +1,115 @@ +import { + createAssertCredentialResultMock, + createCreateCredentialResultMock, + createCredentialCreationOptionsMock, + createCredentialRequestOptionsMock, +} from "../../../autofill/spec/fido2-testing-utils"; +import { WebauthnUtils } from "../webauthn-utils"; + +import { MessageType } from "./messaging/message"; +import { Messenger } from "./messaging/messenger"; + +const originalGlobalThis = globalThis; +const mockGlobalThisDocument = { + ...originalGlobalThis.document, + contentType: "text/html", + location: { + ...originalGlobalThis.document.location, + href: "https://localhost", + origin: "https://localhost", + protocol: "https:", + }, +}; + +let messenger: Messenger; +jest.mock("./messaging/messenger", () => { + return { + Messenger: class extends jest.requireActual("./messaging/messenger").Messenger { + static forDOMCommunication: any = jest.fn((context) => { + const windowOrigin = context.location.origin; + + messenger = new Messenger({ + postMessage: (message, port) => context.postMessage(message, windowOrigin, [port]), + addEventListener: (listener) => context.addEventListener("message", listener), + removeEventListener: (listener) => context.removeEventListener("message", listener), + }); + messenger.destroy = jest.fn(); + return messenger; + }); + }, + }; +}); +jest.mock("../webauthn-utils"); + +describe("Fido2 page script without native WebAuthn support", () => { + (jest.spyOn(globalThis, "document", "get") as jest.Mock).mockImplementation( + () => mockGlobalThisDocument, + ); + + const mockCredentialCreationOptions = createCredentialCreationOptionsMock(); + const mockCreateCredentialsResult = createCreateCredentialResultMock(); + const mockCredentialRequestOptions = createCredentialRequestOptionsMock(); + const mockCredentialAssertResult = createAssertCredentialResultMock(); + require("./page-script"); + + afterEach(() => { + jest.resetModules(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + describe("creating WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialCreationResponse, + result: mockCreateCredentialsResult, + }); + }); + + it("creates and returns a WebAuthn credential", async () => { + await navigator.credentials.create(mockCredentialCreationOptions); + + expect(WebauthnUtils.mapCredentialCreationOptions).toHaveBeenCalledWith( + mockCredentialCreationOptions, + false, + ); + expect(WebauthnUtils.mapCredentialRegistrationResult).toHaveBeenCalledWith( + mockCreateCredentialsResult, + ); + }); + }); + + describe("get WebAuthn credentials", () => { + beforeEach(() => { + messenger.request = jest.fn().mockResolvedValue({ + type: MessageType.CredentialGetResponse, + result: mockCredentialAssertResult, + }); + }); + + it("gets and returns the WebAuthn credentials", async () => { + await navigator.credentials.get(mockCredentialRequestOptions); + + expect(WebauthnUtils.mapCredentialRequestOptions).toHaveBeenCalledWith( + mockCredentialRequestOptions, + false, + ); + expect(WebauthnUtils.mapCredentialAssertResult).toHaveBeenCalledWith( + mockCredentialAssertResult, + ); + }); + }); + + describe("destroy", () => { + it("should destroy the message listener when receiving a disconnect request", async () => { + jest.spyOn(globalThis.top, "removeEventListener"); + const SENDER = "bitwarden-webauthn"; + void messenger.handler({ type: MessageType.DisconnectRequest, SENDER, senderId: "1" }); + + expect(globalThis.top.removeEventListener).toHaveBeenCalledWith("focus", undefined); + expect(messenger.destroy).toHaveBeenCalled(); + }); + }); +}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts deleted file mode 100644 index 8f4efe0330..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.spec.ts +++ /dev/null @@ -1,16 +0,0 @@ -describe("TriggerFido2ContentScriptInjection", () => { - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("init", () => { - it("sends a message to the extension background", () => { - require("../content/trigger-fido2-content-script-injection"); - - expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({ - command: "triggerFido2ContentScriptInjection", - }); - }); - }); -}); diff --git a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts b/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts deleted file mode 100644 index 7ca6956729..0000000000 --- a/apps/browser/src/vault/fido2/content/trigger-fido2-content-script-injection.ts +++ /dev/null @@ -1,5 +0,0 @@ -(function () { - // 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 - chrome.runtime.sendMessage({ command: "triggerFido2ContentScriptInjection" }); -})(); diff --git a/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts new file mode 100644 index 0000000000..287de6804b --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-content-script.enum.ts @@ -0,0 +1,10 @@ +export const Fido2ContentScript = { + PageScript: "content/fido2/page-script.js", + PageScriptAppend: "content/fido2/page-script-append-mv2.js", + ContentScript: "content/fido2/content-script.js", +} as const; + +export const Fido2ContentScriptId = { + PageScript: "fido2-page-script-registration", + ContentScript: "fido2-content-script-registration", +} as const; diff --git a/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts new file mode 100644 index 0000000000..7836247425 --- /dev/null +++ b/apps/browser/src/vault/fido2/enums/fido2-port-name.enum.ts @@ -0,0 +1,3 @@ +export const Fido2PortName = { + InjectedScript: "fido2-injected-content-script-port", +} as const; diff --git a/apps/browser/src/vault/popup/components/vault/collections.component.ts b/apps/browser/src/vault/popup/components/vault/collections.component.ts index c8f85a8b7a..cb37f0fdad 100644 --- a/apps/browser/src/vault/popup/components/vault/collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault/collections.component.ts @@ -5,6 +5,7 @@ import { first } from "rxjs/operators"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -26,6 +27,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { private route: ActivatedRoute, private location: Location, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -34,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } diff --git a/apps/browser/src/popup/settings/folders.component.html b/apps/browser/src/vault/popup/settings/folders.component.html similarity index 95% rename from apps/browser/src/popup/settings/folders.component.html rename to apps/browser/src/vault/popup/settings/folders.component.html index a2b230a692..47cdb0188d 100644 --- a/apps/browser/src/popup/settings/folders.component.html +++ b/apps/browser/src/vault/popup/settings/folders.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/popup/settings/folders.component.ts b/apps/browser/src/vault/popup/settings/folders.component.ts similarity index 100% rename from apps/browser/src/popup/settings/folders.component.ts rename to apps/browser/src/vault/popup/settings/folders.component.ts diff --git a/apps/browser/src/popup/settings/sync.component.html b/apps/browser/src/vault/popup/settings/sync.component.html similarity index 94% rename from apps/browser/src/popup/settings/sync.component.html rename to apps/browser/src/vault/popup/settings/sync.component.html index 6743f12a1a..6d0a1c31a8 100644 --- a/apps/browser/src/popup/settings/sync.component.html +++ b/apps/browser/src/vault/popup/settings/sync.component.html @@ -1,6 +1,6 @@
- diff --git a/apps/browser/src/popup/settings/sync.component.ts b/apps/browser/src/vault/popup/settings/sync.component.ts similarity index 100% rename from apps/browser/src/popup/settings/sync.component.ts rename to apps/browser/src/vault/popup/settings/sync.component.ts diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.html b/apps/browser/src/vault/popup/settings/vault-settings.component.html new file mode 100644 index 0000000000..4928720e46 --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.html @@ -0,0 +1,56 @@ + +
+ +
+

+ {{ "vault" | i18n }} +

+
+ +
+
+
+
+
+ + + + +
+
+
diff --git a/apps/browser/src/vault/popup/settings/vault-settings.component.ts b/apps/browser/src/vault/popup/settings/vault-settings.component.ts new file mode 100644 index 0000000000..a12f6d1d5b --- /dev/null +++ b/apps/browser/src/vault/popup/settings/vault-settings.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; + +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; + +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +@Component({ + selector: "vault-settings", + templateUrl: "vault-settings.component.html", +}) +export class VaultSettingsComponent { + constructor( + public messagingService: MessagingService, + private router: Router, + ) {} + + async import() { + await this.router.navigate(["/import"]); + if (await BrowserApi.isPopupOpen()) { + await BrowserPopupUtils.openCurrentPagePopout(window); + } + } +} diff --git a/apps/browser/src/vault/services/abstractions/fido2.service.ts b/apps/browser/src/vault/services/abstractions/fido2.service.ts deleted file mode 100644 index 138b538b15..0000000000 --- a/apps/browser/src/vault/services/abstractions/fido2.service.ts +++ /dev/null @@ -1,4 +0,0 @@ -export abstract class Fido2Service { - init: () => Promise; - injectFido2ContentScripts: (sender: chrome.runtime.MessageSender) => Promise; -} diff --git a/apps/browser/src/vault/services/fido2.service.spec.ts b/apps/browser/src/vault/services/fido2.service.spec.ts deleted file mode 100644 index 1db2bdfb77..0000000000 --- a/apps/browser/src/vault/services/fido2.service.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import Fido2Service from "./fido2.service"; - -describe("Fido2Service", () => { - let fido2Service: Fido2Service; - let tabMock: chrome.tabs.Tab; - let sender: chrome.runtime.MessageSender; - - beforeEach(() => { - fido2Service = new Fido2Service(); - tabMock = { id: 123, url: "https://bitwarden.com" } as chrome.tabs.Tab; - sender = { tab: tabMock }; - jest.spyOn(BrowserApi, "executeScriptInTab").mockImplementation(); - }); - - afterEach(() => { - jest.resetModules(); - jest.clearAllMocks(); - }); - - describe("injectFido2ContentScripts", () => { - const fido2ContentScript = "content/fido2/content-script.js"; - const defaultExecuteScriptOptions = { runAt: "document_start" }; - - it("accepts an extension message sender and injects the fido2 scripts into the tab of the sender", async () => { - await fido2Service.injectFido2ContentScripts(sender); - - expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, { - file: fido2ContentScript, - ...defaultExecuteScriptOptions, - }); - }); - }); -}); diff --git a/apps/browser/src/vault/services/fido2.service.ts b/apps/browser/src/vault/services/fido2.service.ts deleted file mode 100644 index 98b440b109..0000000000 --- a/apps/browser/src/vault/services/fido2.service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { BrowserApi } from "../../platform/browser/browser-api"; - -import { Fido2Service as Fido2ServiceInterface } from "./abstractions/fido2.service"; - -export default class Fido2Service implements Fido2ServiceInterface { - async init() { - const tabs = await BrowserApi.tabsQuery({}); - tabs.forEach((tab) => { - if (tab.url?.startsWith("https")) { - // 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.injectFido2ContentScripts({ tab } as chrome.runtime.MessageSender); - } - }); - - BrowserApi.addListener(chrome.runtime.onConnect, (port) => { - if (port.name === "fido2ContentScriptReady") { - port.postMessage({ command: "fido2ContentScriptInit" }); - } - }); - } - - /** - * Injects the FIDO2 content script into the current tab. - * @param {chrome.runtime.MessageSender} sender - * @returns {Promise} - */ - async injectFido2ContentScripts(sender: chrome.runtime.MessageSender): Promise { - await BrowserApi.executeScriptInTab(sender.tab.id, { - file: "content/fido2/content-script.js", - frameId: sender.frameId, - runAt: "document_start", - }); - } -} diff --git a/apps/browser/src/vault/services/vault-browser-state.service.ts b/apps/browser/src/vault/services/vault-browser-state.service.ts index a0d55a9d55..43a28928da 100644 --- a/apps/browser/src/vault/services/vault-browser-state.service.ts +++ b/apps/browser/src/vault/services/vault-browser-state.service.ts @@ -52,7 +52,9 @@ export class VaultBrowserStateService { } async setBrowserGroupingsComponentState(value: BrowserGroupingsComponentState): Promise { - await this.activeUserVaultBrowserGroupingsComponentState.update(() => value); + await this.activeUserVaultBrowserGroupingsComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } async getBrowserVaultItemsComponentState(): Promise { @@ -60,6 +62,8 @@ export class VaultBrowserStateService { } async setBrowserVaultItemsComponentState(value: BrowserComponentState): Promise { - await this.activeUserVaultBrowserComponentState.update(() => value); + await this.activeUserVaultBrowserComponentState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } } diff --git a/apps/browser/store/locales/ar/copy.resx b/apps/browser/store/locales/ar/copy.resx index e74606ff15..e1bfa48b44 100644 --- a/apps/browser/store/locales/ar/copy.resx +++ b/apps/browser/store/locales/ar/copy.resx @@ -1,17 +1,17 @@  - @@ -118,42 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدير كلمات مرور مجاني + Bitwarden Password Manager - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - شركة Bitwarden, Inc هي الشركة الأم لشركة 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -تم تصنيف Bitwarden كأفصل مدير كلمات مرور بواسطة كل من The Verge، U.S News & World Report، CNET، وغيرهم. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -قم بادراة وحفظ وتأمين كلمات المرور الخاصة بك، ومشاركتها بين اجهزتك من اي مكان. -يوفر Bitwarden حل مفتوح المصدر لادارة كلمات المرور للجميع، سواء في المنزل، في العمل او في اي مكان. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -يوفر Bitwarden Send امكانية ارسال البيانات --- النصوص والملفات --- بطريقة مشفرة وسريعة لأي شخص. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -يوفر Bitwarden خطط خاصة للفرق والشركات والمؤسسات لتمكنك من مشاركة كلمات المرور مع زملائك في العمل. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -لماذا قد تختار Bitwarden: -تشفير على مستوى عالمي -كلمات المرور محمية بتشفير متقدم تام (end-to-end encryption) من نوعية AES-256 bit، مع salted hashing، و PBKDF2 SHA-256. كل هذا لابقاء بياناتك محمية وخاصة. +More reasons to choose Bitwarden: -مولد كلمات المرور المدمج -قم بانشاء كلمات مرور قوية وفريدة وعشوائية حسب متطلبات الأمان للصفحات التي تزورها. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -الترجمات العالمية -يتوفر Bitwarden باكثر من 40 لغة، وتتنامى الترجمات بفضل مجتمعنا العالمي. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -تطبيقات متعددة المنصات -قم بحماية ومشاركة بياناتاك الحساسة عبر خزنة Bitwarden من اي متصفح ويب، او هاتف ذكي، او جهاز كمبيوتر، وغيرها. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - مدير كلمات مرور مجاني وآمن لجميع أجهزتك + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. مزامنة خزنتك والوصول إليها من عدة أجهزة diff --git a/apps/browser/store/locales/az/copy.resx b/apps/browser/store/locales/az/copy.resx index cb05f8e5d9..2a3d507df2 100644 --- a/apps/browser/store/locales/az/copy.resx +++ b/apps/browser/store/locales/az/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ödənişsiz Parol Meneceri + Bitwarden Password Manager - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc., 8bit Solutions LLC-nin ana şirkətidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VƏ BİR ÇOXUNA GÖRƏ ƏN YAXŞI PAROL MENECERİDİR. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hər yerdən limitsiz cihazda limitsiz parolu idarə edin, saxlayın, qoruyun və paylaşın. Bitwarden evdə, işdə və ya yolda hər kəsə açıq mənbəli parol idarəetmə həllərini təqdim edir. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi parollar yaradın. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send şifrələnmiş məlumatların (fayl və sadə mətnləri) birbaşa və sürətli göndərilməsini təmin edir. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolları iş yoldaşlarınızla təhlükəsiz paylaşa bilməyiniz üçün şirkətlərə Teams və Enterprise planları təklif edir. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Nəyə görə Bitwarden-i seçməliyik: -Yüksək səviyyə şifrələmə -Parollarınız qabaqcıl ucdan-uca şifrələmə (AES-256 bit, salted hashtag və PBKDF2 SHA-256) ilə qorunur, beləcə datanızın güvənli və gizli qalmasını təmin edir. +More reasons to choose Bitwarden: -Daxili parol yaradıcı -Çox istifadə etdiyiniz hər veb sayt üçün təhlükəsizlik tələblərinə görə güclü, unikal və təsadüfi şifrələr yaradın. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Qlobal tərcümələr -Bitwarden tərcümələri 40 dildə mövcuddur və qlobal cəmiyyətimiz sayəsində böyüməyə davam edir. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Çarpaz platform tətbiqləri -Bitwarden anbarındakı həssas verilənləri, istənilən brauzerdən, mobil cihazdan və ya masaüstü əməliyyat sistemindən və daha çoxundan qoruyub paylaşın. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bütün cihazlarınız üçün güvənli və ödənişsiz bir parol meneceri + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Anbarınıza bir neçə cihazdan eyniləşdirərək müraciət edin diff --git a/apps/browser/store/locales/be/copy.resx b/apps/browser/store/locales/be/copy.resx index f84dd699a7..65c337826b 100644 --- a/apps/browser/store/locales/be/copy.resx +++ b/apps/browser/store/locales/be/copy.resx @@ -1,17 +1,17 @@  - @@ -118,24 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бясплатны менеджар пароляў + Bitwarden Password Manager - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden - просты і бяспечны спосаб захоўваць усе вашы імёны карыстальніка і паролі, а таксама лёгка іх сінхранізаваць паміж усімі вашымі прыладамі. Пашырэнне праграмы Bitwarden дазваляе хутка ўвайсці на любы вэб-сайт з дапамогай Safari або Chrome і падтрымліваецца сотнямі іншых папулярных праграм. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Крадзеж пароляў — сур'ёзная праблема. Сайты і праграмы, якія вы выкарыстоўваеце падвяргаюцца нападам кожны дзень. Праблемы ў іх бяспецы могуць прывесці да крадзяжу вашага пароля. Акрамя таго, калі вы выкарыстоўваеце адны і тыя ж паролі на розных сайтах і праграмах, то хакеры могуць лёгка атрымаць доступ да некалькіх вашых уліковых запісаў адразу (да паштовай скрыні, да банкаўскага рахунку ды г. д.). +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Эксперты па бяспецы рэкамендуюць выкарыстоўваць розныя выпадкова знегерыраваныя паролі для кожнага створанага вамі ўліковага запісу. Але як жа кіраваць усімі гэтымі паролямі? Bitwarden дазваляе вам лёгка атрымаць доступ да вашых пароляў, а гэтак жа ствараць і захоўваць іх. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Bitwarden захоўвае ўсе вашы імёны карыстальніка і паролі ў зашыфраваным сховішчы, якое сінхранізуецца паміж усімі вашымі прыладамі. Да таго, як даныя пакінуць вашу прыладу, яны будуць зашыфраваны і толькі потым адпраўлены. Мы ў Bitwarden не зможам прачытаць вашы даныя, нават калі мы гэтага захочам. Вашы даныя зашыфраваны пры дапамозе алгарытму AES-256 і PBKDF2 SHA-256. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden — гэта праграмнае забеспячэнне з адкрытым на 100% зыходным кодам. Зыходны код Bitwarden размешчаны на GitHub, і кожны можа свабодна праглядаць, правяраць і рабіць унёсак у код Bitwarden. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Бяспечны і бясплатны менеджар пароляў для ўсіх вашых прылад + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Сінхранізацыя і доступ да сховішча з некалькіх прылад diff --git a/apps/browser/store/locales/bg/copy.resx b/apps/browser/store/locales/bg/copy.resx index 29c468f045..bc08f6a107 100644 --- a/apps/browser/store/locales/bg/copy.resx +++ b/apps/browser/store/locales/bg/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — безплатен управител на пароли + Bitwarden Password Manager - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - „Bitwarden, Inc.“ е компанията-майка на „8bit Solutions LLC“. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ОПРЕДЕЛЕН КАТО НАЙ-ДОБРИЯТ УПРАВИТЕЛ НА ПАРОЛИ ОТ „THE VERGE“, „U.S. NEWS & WORLD REPORT“, „CNET“ И ОЩЕ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управлявайте, съхранявайте, защитавайте и споделяйте неограничен брой пароли на неограничен брой устройства от всяка точка на света. Битуорден предоставя решение за управление на паролите с отворен код, от което може да се възползва всеки, било то у дома, на работа или на път. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -С Изпращанията на Битуорден можете незабавно да предавате шифрована информация под формата на файлове и обикновен текст – директно и с всекиго. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Битуорден предлага планове за екипи и големи фирми, така че служителите в компаниите да могат безопасно да споделят пароли помежду си. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Защо да изберете Битуорден: -Шифроване от най-висока класа -Паролите са защитени със сложен шифър „от край до край“ (AES-256 bit, salted hashtag и PBKDF2 SHA-256), така че данните Ви остават да са защитени и неприкосновени. +More reasons to choose Bitwarden: -Вграден генератор на пароли -Създавайте сигурни, уникални и случайни пароли според изискванията за сигурност на всеки уеб сайт, който посещавате. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобални преводи -Битуорден е преведен на 40 езика и този брой не спира да расте, благодарение на нашата глобална общност. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Приложения за всяка система -Защитете и споделяйте поверителните данни от своя трезор в Битуорден от всеки браузър, мобилно устройство или компютър. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурен и свободен управител на пароли за всички устройства + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Удобен достъп до трезора, който се синхронизира от всички устройства diff --git a/apps/browser/store/locales/bn/copy.resx b/apps/browser/store/locales/bn/copy.resx index a8eb4f7c75..1bcfb19001 100644 --- a/apps/browser/store/locales/bn/copy.resx +++ b/apps/browser/store/locales/bn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + Bitwarden Password Manager - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - আপনার সমস্ত ডিভাইসের জন্য একটি সুরক্ষিত এবং বিনামূল্যের পাসওয়ার্ড ব্যবস্থাপক + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. একাধিক ডিভাইস থেকে আপনার ভল্ট সিঙ্ক এবং ব্যাবহার করুন diff --git a/apps/browser/store/locales/bs/copy.resx b/apps/browser/store/locales/bs/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/bs/copy.resx +++ b/apps/browser/store/locales/bs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/ca/copy.resx b/apps/browser/store/locales/ca/copy.resx index 0bd454addb..5a06f818e3 100644 --- a/apps/browser/store/locales/ca/copy.resx +++ b/apps/browser/store/locales/ca/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Administrador de contrasenyes gratuït + Bitwarden - Gestor de contrasenyes - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible. - Bitwarden, Inc. és la companyia matriu de solucions de 8 bits LLC. + Reconegut com el millor gestor de contrasenyes per PCMag, WIRED, The Verge, CNET, G2 i més! -Nomenada Millor gestor de contrasenyes per THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +ASSEGURA LA TEUA VIDA DIGITAL +Assegureu-vos la vostra vida digital i protegiu-vos de les violacions de dades generant i desant contrasenyes úniques i fortes per a cada compte. Mantingueu-ho tot en una caixa de contrasenyes xifrada d'extrem a extrem a la qual només podeu accedir. -Gestioneu, emmagatzemeu, segures i compartiu contrasenyes il·limitades a través de dispositius il·limitats des de qualsevol lloc. Bitwarden lliura solucions de gestió de contrasenyes de codi obert a tothom, ja siga a casa, a la feina o sobre la marxa. +ACCEDEIX A LES SEUES DADES, ON, EN QUALSEVOL MOMENT, EN QUALSEVOL DISPOSITIU +Gestiona, emmagatzema, protegeix i comparteix fàcilment contrasenyes il·limitades en dispositius il·limitats sense restriccions. -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +TOTHOM HA DE TENIR LES EINES PER ESTAR SEGURETAT EN LÍNIA +Utilitzeu Bitwarden de forma gratuïta sense anuncis ni dades de venda. Bitwarden creu que tothom hauria de tenir la capacitat de mantenir-se segur en línia. Els plans Prèmium ofereixen accés a funcions avançades. -Bitwarden Send transmet ràpidament informació xifrada --- Fitxers i text complet - directament a qualsevol persona. +EMPODERA ELS TEUS EQUIPS AMB BITWARDEN +Els plans per a equips i empreses inclouen funcions empresarials professionals. Alguns exemples inclouen integració SSO, autoallotjament, integració de directoris i subministrament SCIM, polítiques globals, accés a API, registres d'esdeveniments i molt més. -Bitwarden ofereix equips i plans empresarials per a empreses perquè pugueu compartir amb seguretat contrasenyes amb els companys. +Utilitzeu Bitwarden per protegir la vostra força de treball i compartir informació crítica amb els companys. -Per què triar Bitwarden: + +Més raons per a triar Bitwarden: Xifratge de classe mundial -Les contrasenyes estan protegides amb un xifratge avançat fi-a-fi (AES-256 bit, salted hashtag, and PBKDF2 SHA-256), de manera que les vostres dades es mantenen segures i privades. +Les contrasenyes estan protegides amb un xifratge avançat d'extrem a extrem (AES-256 bits, hashtag salat i PBKDF2 SHA-256) perquè les vostres dades es mantinguen segures i privades. -Generador de contrasenyes integrat -Genereu contrasenyes fortes, úniques i aleatòries basades en els requisits de seguretat per a cada lloc web que freqüenteu. +Auditories de tercers +Bitwarden realitza regularment auditories de seguretat exhaustives de tercers amb empreses de seguretat notables. Aquestes auditories anuals inclouen avaluacions del codi font i proves de penetració a les IP, servidors i aplicacions web de Bitwarden. + +2FA avançat +Assegureu el vostre inici de sessió amb un autenticador de tercers, codis enviats per correu electrònic o credencials FIDO2 WebAuthn, com ara una clau de seguretat de maquinari o una clau de pas. + +Bitwarden Enviar +Transmet dades directament a altres, mantenint la seguretat xifrada d'extrem a extrem i limitant l'exposició. + +Generador incorporat +Creeu contrasenyes llargues, complexes i diferents i noms d'usuari únics per a cada lloc que visiteu. Integració amb proveïdors d'àlies de correu electrònic per obtenir més privadesa. Traduccions globals -Les traduccions de Bitwarden existeixen en 40 idiomes i creixen, gràcies a la nostra comunitat global. +Les traduccions de Bitwarden existeixen per a més de 60 idiomes, traduïdes per la comunitat global mitjançant Crowdin. -Aplicacions de plataforma creuada -Assegureu-vos i compartiu dades sensibles a la vostra caixa forta de Bitwarden des de qualsevol navegador, dispositiu mòbil o S.O. d'escriptori, i molt més. +Aplicacions multiplataforma +Assegureu-vos i compartiu dades confidencials a la vostra caixa forta Bitwarden des de qualsevol navegador, dispositiu mòbil o sistema operatiu d'escriptori i molt més. + +Bitwarden assegura més que només contrasenyes +Les solucions de gestió de credencials xifrades d'extrem a extrem de Bitwarden permeten a les organitzacions protegir-ho tot, inclosos els secrets dels desenvolupadors i les experiències de clau de pas. Visiteu Bitwarden.com per obtenir més informació sobre Bitwarden gestor de secrets i Bitwarden Passwordless.dev! - Administrador de contrasenyes segur i gratuït per a tots els vostres dispositius + A casa, a la feina o en moviment, Bitwarden protegeix totes les contrasenyes, claus de pas i informació sensible. Sincronitzeu i accediu a la vostra caixa forta des de diversos dispositius diff --git a/apps/browser/store/locales/cs/copy.resx b/apps/browser/store/locales/cs/copy.resx index 6b711e2863..c3a58379dc 100644 --- a/apps/browser/store/locales/cs/copy.resx +++ b/apps/browser/store/locales/cs/copy.resx @@ -1,17 +1,17 @@  - @@ -118,47 +118,64 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správce hesel + Bitwarden - Správce hesel - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách. - Bitwarden, Inc. je mateřskou společností 8bit Solutions LLC. + PCMag, WIRED, The Verge, CNET, G2 a další ocenili tohoto správce hesel jako nejlepší! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET A DALŠÍ JI OZNAČILY ZA NEJLEPŠÍHO SPRÁVCE HESEL. +ZABEZPEČTE SVŮJ DIGITÁLNÍ ŽIVOT +Zabezpečte svůj digitální život a chraňte se před únikem dat tím, že si pro každý účet vytvoříte a uložíte jedinečná, silná hesla. Vše uchováváte v end-to-end šifrovaném trezoru hesel, ke kterému máte přístup jen Vy. -Spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení odkudkoliv. Bitwarden poskytuje open source řešení pro správu hesel všem, ať už doma, v práci nebo na cestách. +PŘÍSTUP K DATŮM ODKUDKOLI, KDYKOLI A Z JAKÉHOKOLI ZAŘÍZENÍ +Snadno spravujte, ukládejte, zabezpečujte a sdílejte neomezený počet hesel na neomezeném počtu zařízení bez omezení. -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +KAŽDÝ BY MĚL MÍT K DISPOZICI NÁSTROJE, KTERÉ MU UMOŽNÍ ZŮSTAT V BEZPEČÍ ONLINE +Využívejte Bitwarden zdarma bez reklam a prodeje dat. Bitwarden věří, že každý by měl mít možnost zůstat v bezpečí online. Prémiové plány nabízejí přístup k pokročilým funkcím. -Bitwarden Send rychle přenáší šifrované informace --- soubory a prostý text -- přímo a komukoli. +POSILTE SVÉ TÝMY POMOCÍ BITWARDEN +Plány pro týmy a podniky jsou vybaveny profesionálními podnikovými funkcemi. Mezi příklady patří integrace SSO, selfhosting, integrace adresářů a poskytování SCIM, globální zásady, přístup k API, protokoly událostí a další. -Bitwarden nabízí plány Teams a Enterprise pro firmy, takže můžete bezpečně sdílet hesla s kolegy. +Použijte Bitwarden k zabezpečení svých zaměstnanců a sdílení citlivých informací s kolegy. -Proč si vybrat Bitwarden: + +Další důvody, proč si vybrat Bitwarden: Šifrování na světové úrovni -Hesla jsou chráněna pokročilým koncovým šifrováním (AES-256 bit, salted hashování a PBKDF2 SHA-256), takže Vaše data zůstanou bezpečná a soukromá. +Hesla jsou chráněna pokročilým end-to-end šifrováním (AES-256 bitů, solený hashtag a PBKDF2 SHA-256), takže Vaše data zůstanou v bezpečí a soukromí. -Vestavěný generátor hesel -Generujte silná, jedinečná a náhodná hesla na základě bezpečnostních požadavků pro každou webovou stránku, kterou navštěvujete. +Audity třetích stran +Společnost Bitwarden pravidelně provádí komplexní bezpečnostní audity třetích stran s významnými bezpečnostními firmami. Tyto každoroční audity zahrnují posouzení zdrojového kódu a penetrační testy napříč IP adresami, servery a webovými aplikacemi společnosti Bitwarden. + +Pokročilé 2FA +Zabezpečte své přihlášení pomocí ověření třetí strany, e-mailových kódů nebo ověření FIDO2 WebAuthn, jako je hardwarový bezpečnostní klíč nebo přístupový klíč. + +Bitwarden Send +Přenášejte data přímo ostatním při zachování end-to-end šifrovaného zabezpečení a omezení odhalení. + +Vestavěný generátor +Vytvářejte dlouhá, složitá a odlišná hesla a jedinečná uživatelská jména pro každou navštívenou stránku. Integrace s poskytovateli e-mailových aliasů pro zajištění dalšího soukromí. Globální překlady -Překlady Bitwarden existují ve 40 jazycích a díky naší globální komunitě se stále rozšiřují. +Pro Bitwarden existují překlady ve více než 60 jazycích, které překládá globální komunita prostřednictvím služby Crowdin. Aplikace pro více platforem -Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo operačního systému pro počítač. +Zabezpečte a sdílejte citlivá data v rámci svého trezoru Bitwarden z jakéhokoli prohlížeče, mobilního zařízení nebo desktopového operačního systému a dalších. + +Bitwarden zabezpečuje více než jen hesla +Řešení pro komplexní správu šifrovaných pověření od společnosti Bitwarden umožňují organizacím zabezpečit vše, včetně přístupových a/nebo tajných klíčů pro vývojáře. Navštivte Bitwarden.com a dozvíte se více o Bitwarden Secrets Manager a Bitwarden Passwordless.dev! - Bezpečný a bezplatný správce hesel pro všechna Vaše zařízení + Bitwarden zabezpečí všechna Vaše hesla, přístupové klíče a citlivé informace doma, v práci nebo na cestách. Synchronizujte a přistupujte ke svému trezoru z různých zařízení - Spravujte veškeré své přihlašovací údaje z bezpečného trezoru + Spravujte veškeré své přihlašovací údaje v bezpečném trezoru Rychle vyplňte své přihlašovací údaje na webových stránkách diff --git a/apps/browser/store/locales/cy/copy.resx b/apps/browser/store/locales/cy/copy.resx index 8222329630..983a112c07 100644 --- a/apps/browser/store/locales/cy/copy.resx +++ b/apps/browser/store/locales/cy/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Rheolydd cyfineiriau am ddim + Bitwarden Password Manager - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yw rhiant-gwmni 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Y RHEOLYDD CYFRINEIRIAU GORAU YN ÔL THE VERGE, US NEWS & WORLD REPORT, CNET, A MWY. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Rheolwch, storiwch, diogelwch a rhannwch gyfrineiriau di-ri ar draws dyfeiriau di-ri o unrhyw le. Mae Bitwarden yn cynnig rhaglenni rheoli cyfrineiriau cod-agored i bawb, boed gartref, yn y gwaith, neu ar fynd. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Mae Bitwarden Send yn trosglwyddo gwybodaeth wedi'i hamgryptio yn gyflym -- ffeiliau a thestun plaen -- yn uniongyrchol i unrhyw un. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Mae Bitwarden yn cynnig cynlluniau Teams ac Enterprise i gwmnïau er mwyn i chi allu rhannu cyfrineiriau gyda chydweithwyr yn ddiogel. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pam dewis Bitwarden: -Amgryptio o'r radd flaenaf -Mae cyfrineiriau wedi'u hamddiffyn ag amgryptio datblygedig o un pen i'r llall (AES-256 bit, hashio â halen, a PBKDF2 SHA-256) er mwyn i'ch data aros yn ddiogel ac yn breifat. +More reasons to choose Bitwarden: -Cynhyrchydd cyfrineiriau -Gallwch gynhyrchu cyfrineiriau cryf, unigryw ac ar hap yn seiliedig ar ofynion diogelwch ar gyfer pob gwefan rydych chi'n ei defnyddio. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Ar gael yn eich iaith chi -Mae Bitwarden wedi'i gyfieithu i dros 40 o ieithoedd, diolch i'n cymuned fyd-eang. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Rhaglenni traws-blatfform -Diogelwch a rhannwch ddata sensitif yn eich cell Bitwarden o unrhyw borwr, dyfais symudol, neu system weithredu, a mwy. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Rheolydd diogel a rhad ac am ddim ar gyfer eich holl ddyfeisiau + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Gallwch gael mynediad at, a chysoni, eich cell o sawl dyfais diff --git a/apps/browser/store/locales/da/copy.resx b/apps/browser/store/locales/da/copy.resx index 858c56dea9..775a3edd81 100644 --- a/apps/browser/store/locales/da/copy.resx +++ b/apps/browser/store/locales/da/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis adgangskodemanager + Bitwarden Password Manager - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. er moderselskab for 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UDNÆVNT BEDSTE PASSWORD MANAGER AF THE VERGE, U.S. NEWS & WORLD REPORT, CNET OG FLERE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administrér, gem, sikr og del adgangskoder ubegrænset på tværs af enheder hvor som helst. Bitwarden leverer open source adgangskodeadministrationsløsninger til alle, hvad enten det er hjemme, på arbejdspladsen eller på farten. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send overfører hurtigt krypterede oplysninger --- filer og almindelig tekst - direkte til enhver. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden tilbyder Teams og Enterprise-planer for virksomheder, så du sikkert kan dele adgangskoder med kolleger. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Hvorfor vælge Bitwarden: -Kryptering i verdensklasse -Adgangskoder er beskyttet med avanceret end-to-end-kryptering (AES-256 bit, saltet hashing og PBKDF2 SHA-256), så dine data forbliver sikre og private. +More reasons to choose Bitwarden: -Indbygget adgangskodegenerator -Generér stærke, unikke og tilfældige adgangskoder baseret på sikkerhedskrav til hvert websted, du besøger. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globale oversættelser -Bitwarden findes på 40 sprog, og flere kommer til, takket være vores globale fællesskab. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applikationer på tværs af platforme -Beskyt og del følsomme data i din Bitwarden boks fra enhver browser, mobilenhed eller desktop OS og mere. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og gratis adgangskodemanager til alle dine enheder + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få adgang til din boks fra flere enheder diff --git a/apps/browser/store/locales/de/copy.resx b/apps/browser/store/locales/de/copy.resx index 139a6026fd..eb3ab2afd4 100644 --- a/apps/browser/store/locales/de/copy.resx +++ b/apps/browser/store/locales/de/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Kostenloser Passwort-Manager + Bitwarden Passwort-Manager - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. - Bitwarden, Inc. ist die Muttergesellschaft von 8bit Solutions LLC. + Ausgezeichnet als bester Passwortmanager von PCMag, WIRED, The Verge, CNET, G2 und vielen anderen! -AUSGEZEICHNET ALS BESTER PASSWORTMANAGER VON THE VERGE, U.S. NEWS & WORLD REPORT, CNET UND ANDEREN. +SCHÜTZE DEIN DIGITALES LEBEN +Sicher dein digitales Leben und schütze dich vor Passwortdiebstählen, indem du individuelle, sichere Passwörter für jedes Konto erstellest und speicherst. Verwalte alles in einem Ende-zu-Ende verschlüsselten Passwort-Tresor, auf den nur du Zugriff hast. -Verwalte, speichere, sichere und teile unbegrenzte Passwörter von überall auf unbegrenzten Geräten. Bitwarden liefert Open-Source-Passwort-Management-Lösungen für alle, sei es zu Hause, am Arbeitsplatz oder unterwegs. +ZUGRIFF AUF DEINE DATEN, ÜBERALL, JEDERZEIT UND AUF JEDEM GERÄT +Verwalte, speichere, sichere und teile einfach eine unbegrenzte Anzahl von Passwörtern auf einer unbegrenzten Anzahl von Geräten ohne Einschränkungen. -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +JEDER SOLLTE DIE MÖGLICHKEIT HABEN, ONLINE GESCHÜTZT ZU BLEIBEN +Verwende Bitwarden kostenlos, ohne Werbung oder Datenverkauf. Bitwarden glaubt, dass jeder die Möglichkeit haben sollte, online geschützt zu bleiben. Premium-Abos bieten Zugang zu erweiterten Funktionen. -Bitwarden Send überträgt schnell verschlüsselte Informationen - Dateien und Klartext - direkt an jeden. +STÄRKE DEINE TEAMS MIT BITWARDEN +Tarife für Teams und Enterprise enthalten professionelle Business-Funktionen. Einige Beispiele sind SSO-Integration, Selbst-Hosting, Directory-Integration und SCIM-Bereitstellung, globale Richtlinien, API-Zugang, Ereignisprotokolle und mehr. -Bitwarden bietet Teams und Enterprise Pläne für Unternehmen an, damit du Passwörter sicher mit Kollegen teilen kannst. +Nutze Bitwarden, um deine Mitarbeiter abzusichern und sensible Informationen mit Kollegen zu teilen. -Warum Bitwarden: -Weltklasse Verschlüsselung -Passwörter sind durch erweiterte Ende-zu-Ende-Verschlüsselung (AES-256 Bit, salted hashing und PBKDF2 SHA-256) so bleiben deine Daten sicher und privat. +Weitere Gründe, Bitwarden zu wählen: -Integrierter Passwortgenerator -Generiere starke, einzigartige und zufällige Passwörter basierend auf Sicherheitsanforderungen für jede Website, die du häufig besuchst. +Weltklasse-Verschlüsselung +Passwörter werden mit fortschrittlicher Ende-zu-Ende-Verschlüsselung (AES-256 bit, salted hashtag und PBKDF2 SHA-256) geschützt, damit deine Daten sicher und geheim bleiben. -Globale Übersetzungen -Bitwarden Übersetzungen existieren in 40 Sprachen und wachsen dank unserer globalen Community. +3rd-Party-Prüfungen +Bitwarden führt regelmäßig umfassende Sicherheitsprüfungen durch Dritte von namhaften Sicherheitsfirmen durch. Diese jährlichen Prüfungen umfassen Quellcode-Bewertungen und Penetration-Tests für Bitwarden-IPs, Server und Webanwendungen. -Plattformübergreifende Anwendungen -Sichere und teile vertrauliche Daten in deinem Bitwarden Tresor von jedem Browser, jedem mobilen Gerät oder Desktop-Betriebssystem und mehr. +Erweiterte 2FA +Schütze deine Zugangsdaten mit einem Authentifikator eines Drittanbieters, per E-Mail verschickten Codes oder FIDO2 WebAuthn-Zugangsadaten wie einem Hardware-Sicherheitsschlüssel oder Passkey. + +Bitwarden Send +Übertrage Daten direkt an andere, während die Ende-zu-Ende-Verschlüsselung beibehalten wird und die Verbreitung begrenzt werden kann. + +Eingebauter Generator +Erstelle lange, komplexe und eindeutige Passwörter und eindeutige Benutzernamen für jede Website, die du besuchst. Integriere E-Mail-Alias-Anbieter für zusätzlichen Datenschutz. + +Globale Übersetzungen +Es gibt Bitwarden-Übersetzungen für mehr als 60 Sprachen, die von der weltweiten Community über Crowdin übersetzt werden. + +Plattformübergreifende Anwendungen +Schütze und teile sensible Daten in deinem Bitwarden Tresor von jedem Browser, mobilen Gerät oder Desktop-Betriebssystem und mehr. + +Bitwarden schützt mehr als nur Passwörter +Ende-zu-Ende verschlüsselte Zugangsverwaltungs-Lösungen von Bitwarden ermöglicht es Organisationen, alles zu sichern, einschließlich Entwicklergeheimnissen und Passkeys. Besuche Bitwarden.com, um mehr über den Bitwarden Secrets Manager und Bitwarden Passwordless.dev zu erfahren! - Ein sicherer und kostenloser Passwort-Manager für all deine Geräte + Zu Hause, am Arbeitsplatz oder unterwegs schützt Bitwarden einfach alle deine Passwörter, Passkeys und vertraulichen Informationen. Synchronisiere und greife auf deinen Tresor von unterschiedlichen Geräten aus zu diff --git a/apps/browser/store/locales/el/copy.resx b/apps/browser/store/locales/el/copy.resx index 01def6ea5a..fb50f95bdc 100644 --- a/apps/browser/store/locales/el/copy.resx +++ b/apps/browser/store/locales/el/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Δωρεάν Διαχειριστής Κωδικών + Bitwarden Password Manager - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Η Bitwarden, Inc. είναι η μητρική εταιρεία της 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ΟΝΟΜΑΣΘΗΚΕ ΩΣ Ο ΚΑΛΥΤΕΡΟΣ ΔΙΑΧΕΙΡΙΣΤΗΣ ΚΩΔΙΚΩΝ ΠΡΟΣΒΑΣΗΣ ΑΠΟ ΤΟ VERGE, το U.S. NEWS & WORLD REPORT, το CNET και άλλα. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Διαχειριστείτε, αποθηκεύστε, ασφαλίστε και μοιραστείτε απεριόριστους κωδικούς πρόσβασης σε απεριόριστες συσκευές από οπουδήποτε. Το Bitwarden παρέχει λύσεις διαχείρισης κωδικών πρόσβασης ανοιχτού κώδικα σε όλους, στο σπίτι, στη δουλειά ή εν κινήσει. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Το Bitwarden Send αποστέλλει γρήγορα κρυπτογραφημένες πληροφορίες --- αρχεία και απλό κείμενο -- απευθείας σε οποιονδήποτε. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Το Bitwarden προσφέρει προγράμματα Teams και Enterprise για εταιρείες, ώστε να μπορείτε να μοιράζεστε με ασφάλεια τους κωδικούς πρόσβασης με τους συναδέλφους σας. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Γιατί να επιλέξετε το Bitwarden: -Κρυπτογράφηση παγκόσμιας κλάσης -Οι κωδικοί πρόσβασης προστατεύονται με προηγμένη end-to-end κρυπτογράφηση (AES-256 bit, salted hashing και PBKDF2 SHA-256), ώστε τα δεδομένα σας να παραμένουν ασφαλή και ιδιωτικά. +More reasons to choose Bitwarden: -Ενσωματωμένη Γεννήτρια Κωδικών Πρόσβασης -Δημιουργήστε ισχυρούς, μοναδικούς και τυχαίους κωδικούς πρόσβασης βάσει των απαιτήσεων ασφαλείας, για κάθε ιστότοπο που συχνάζετε. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Παγκόσμιες Μεταφράσεις -Υπάρχουν μεταφράσεις για το Bitwarden σε 40 γλώσσες και αυξάνονται συνεχώς, χάρη στην παγκόσμια κοινότητά μας. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Εφαρμογές για όλες τις πλατφόρμες -Ασφαλίστε και μοιραστείτε ευαίσθητα δεδομένα εντός του Bitwarden Vault από οποιοδήποτε πρόγραμμα περιήγησης, κινητή συσκευή ή λειτουργικό σύστημα υπολογιστών, και πολλά άλλα. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Ένας ασφαλής και δωρεάν διαχειριστής κωδικών για όλες τις συσκευές σας + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Συγχρονίστε και αποκτήστε πρόσβαση στο θησαυροφυλάκιό σας από πολλαπλές συσκευές diff --git a/apps/browser/store/locales/en/copy.resx b/apps/browser/store/locales/en/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/en/copy.resx +++ b/apps/browser/store/locales/en/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/en_GB/copy.resx b/apps/browser/store/locales/en_GB/copy.resx index 191198691d..7c408ad889 100644 --- a/apps/browser/store/locales/en_GB/copy.resx +++ b/apps/browser/store/locales/en_GB/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognised as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-Party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community through Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/en_IN/copy.resx b/apps/browser/store/locales/en_IN/copy.resx index 191198691d..31e5d2326f 100644 --- a/apps/browser/store/locales/en_IN/copy.resx +++ b/apps/browser/store/locales/en_IN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilise Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organisations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/es/copy.resx b/apps/browser/store/locales/es/copy.resx index dc7484777a..019006422a 100644 --- a/apps/browser/store/locales/es/copy.resx +++ b/apps/browser/store/locales/es/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestor de contraseñas gratuito + Bitwarden - Administrador de contraseñas - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + En casa, en el trabajo o en el viaje, Bitwarden asegura fácilmente todas sus contraseñas, claves de acceso e información confidencial. - Bitwarden, Inc. es la empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMBRADO MEJOR ADMINISTRADOR DE CONTRASEÑAS POR THE VERGE, U.S. NEWS & WORLD REPORT, CNET Y MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, proteja y comparta contraseñas ilimitadas en dispositivos ilimitados desde cualquier lugar. Bitwarden ofrece soluciones de gestión de contraseñas de código abierto para todos, ya sea en casa, en el trabajo o en mientras estás de viaje. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genere contraseñas seguras, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rápidamente información cifrada --- archivos y texto sin formato, directamente a cualquier persona. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece planes Teams y Enterprise para empresas para que pueda compartir contraseñas de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -¿Por qué elegir Bitwarden? -Cifrado de clase mundial -Las contraseñas están protegidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing y PBKDF2 SHA-256) para que sus datos permanezcan seguros y privados. +More reasons to choose Bitwarden: -Generador de contraseñas incorporado -Genere contraseñas fuertes, únicas y aleatorias en función de los requisitos de seguridad de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducciones Globales -Las traducciones de Bitwarden existen en 40 idiomas y están creciendo, gracias a nuestra comunidad global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicaciones multiplataforma -Proteja y comparta datos confidenciales dentro de su Caja Fuerte de Bitwarden desde cualquier navegador, dispositivo móvil o sistema operativo de escritorio, y más. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestor de contraseñas seguro y gratuito para todos tus dispositivos + En casa, en el trabajo o mientras viaja, Bitwarden protege fácilmente todas sus contraseñas, claves de acceso e información confidencial. Sincroniza y accede a tu caja fuerte desde múltiples dispositivos diff --git a/apps/browser/store/locales/et/copy.resx b/apps/browser/store/locales/et/copy.resx index 2014ec88a8..eccbeba1ed 100644 --- a/apps/browser/store/locales/et/copy.resx +++ b/apps/browser/store/locales/et/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Tasuta paroolihaldur + Bitwarden Password Manager - Tasuta ja turvaline paroolihaldur kõikidele sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tasuta ja turvaline paroolihaldur kõikidele Sinu seadmetele + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sünkroniseeri ja halda oma kontot erinevates seadmetes diff --git a/apps/browser/store/locales/eu/copy.resx b/apps/browser/store/locales/eu/copy.resx index e5b3d542e3..e4271e8ae3 100644 --- a/apps/browser/store/locales/eu/copy.resx +++ b/apps/browser/store/locales/eu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Doaneko pasahitz kudeatzailea + Bitwarden Password Manager - Bitwarden, zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 8bit Solutions LLC-ren enpresa matrizea da. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT ETA CNET ENPRESEK PASAHITZ-ADMINISTRATZAILE ONENA izendatu dute, besteak beste. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gailu guztien artean pasahitz mugagabeak kudeatu, biltegiratu, babestu eta partekatzen ditu. Bitwardenek kode irekiko pasahitzak administratzeko irtenbideak eskaintzen ditu, bai etxean, bai lanean, bai bidaiatzen ari zaren bitartean. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, webgune bakoitzaren segurtasun-baldintzetan oinarrituta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send-ek azkar transmititzen du zifratutako informazioa --- artxiboak eta testu sinplea -- edozein pertsonari zuzenean. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden-ek Taldeak eta Enpresak planak eskaintzen ditu, enpresa bereko lankideek pasahitzak modu seguruan parteka ditzaten. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zergatik aukeratu Bitwarden: -Mundu-mailako zifratzea -Pasahitzak muturretik muturrerako zifratze aurreratuarekin babestuta daude (AES-256 bit, salted hashtag eta PBKDF2 SHA-256), zure informazioa seguru eta pribatu egon dadin. +More reasons to choose Bitwarden: -Pasahitzen sortzailea -Pasahitz sendoak, bakarrak eta ausazkoak sortzen ditu, web gune bakoitzaren segurtasun-baldintzetan oinarrituta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Itzulpenak -Bitwarden 40 hizkuntzatan dago, eta gero eta gehiago dira, gure komunitate globalari esker. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plataforma anitzeko aplikazioak -Babestu eta partekatu zure Bitwarden kutxa gotorraren informazio konfidentziala edozein nabigatzailetatik, gailu mugikorretatik, mahaigaineko aplikaziotik eta gehiagotatik. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Zure gailu guztietarako pasahitzen kudeatzaile seguru eta doakoa + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronizatu eta sartu zure kutxa gotorrean hainbat gailutatik diff --git a/apps/browser/store/locales/fa/copy.resx b/apps/browser/store/locales/fa/copy.resx index 23cb3f3bf0..67095435ac 100644 --- a/apps/browser/store/locales/fa/copy.resx +++ b/apps/browser/store/locales/fa/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - مدیریت کلمه عبور رایگان + Bitwarden Password Manager - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden، Inc. شرکت مادر 8bit Solutions LLC است. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -به عنوان بهترین مدیر کلمه عبور توسط VERGE، US News & WORLD REPORT، CNET و دیگران انتخاب شد. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -کلمه‌های عبور با تعداد نامحدود را در دستگاه‌های نامحدود از هر کجا مدیریت کنید، ذخیره کنید، ایمن کنید و به اشتراک بگذارید. Bitwarden راه حل های مدیریت رمز عبور منبع باز را به همه ارائه می دهد، چه در خانه، چه در محل کار یا در حال حرکت. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send به سرعت اطلاعات رمزگذاری شده --- فایل ها و متن ساده - را مستقیماً به هر کسی منتقل می کند. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden برنامه‌های Teams و Enterprise را برای شرکت‌ها ارائه می‌دهد تا بتوانید به‌طور ایمن کلمه‌های را با همکاران خود به اشتراک بگذارید. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -چرا Bitwarden را انتخاب کنید: -رمزگذاری در کلاس جهانی -کلمه‌های عبور با رمزگذاری پیشرفته (AES-256 بیت، هشتگ سالت و PBKDF2 SHA-256) محافظت می‌شوند تا داده‌های شما امن و خصوصی بمانند. +More reasons to choose Bitwarden: -تولیدکننده کلمه عبور داخلی -بر اساس الزامات امنیتی برای هر وب سایتی که بازدید می کنید، کلمه‌های عبور قوی، منحصر به فرد و تصادفی ایجاد کنید. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ترجمه های جهانی -ترجمه Bitwarden به 40 زبان وجود دارد و به لطف جامعه جهانی ما در حال رشد است. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -برنامه های کاربردی چند پلتفرمی -داده‌های حساس را در Bitwarden Vault خود از هر مرورگر، دستگاه تلفن همراه یا سیستم عامل دسکتاپ و غیره ایمن کنید و به اشتراک بگذارید. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - یک مدیریت کننده کلمه عبور رایگان برای تمامی دستگاه‌هایتان + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. همگام‌سازی و دسترسی به گاوصندوق خود از دستگاه های مختلف diff --git a/apps/browser/store/locales/fi/copy.resx b/apps/browser/store/locales/fi/copy.resx index 4440603239..a50cedbdac 100644 --- a/apps/browser/store/locales/fi/copy.resx +++ b/apps/browser/store/locales/fi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Ilmainen salasanahallinta + Bitwarden Salasanahallinta - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi. - Bitwarden, Inc. on 8bit Solutions LLC:n emoyhtiö. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NIMENNYT PARHAAKSI SALASANOJEN HALLINNAKSI MM. THE VERGE, U.S. NEWS & WORLD REPORT JA CNET. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hallinnoi, säilytä, suojaa ja jaa salasanoja rajattomalta laitemäärältä mistä tahansa. Bitwarden tarjoaa avoimeen lähdekoodin perustuvan salasanojen hallintaratkaisun kaikille, olitpa sitten kotona, töissä tai liikkeellä. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Luo usein käyttämillesi sivustoille automaattisesti vahvoja, yksilöllisiä ja satunnaisia salasanoja. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send -ominaisuudella lähetät tietoa nopeasti salattuna — tiedostoja ja tekstiä — suoraan kenelle tahansa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Yritystoimintaan Bitwarden tarjoaa yrityksille Teams- ja Enterprise-tilaukset, jotta salasanojen jakaminen kollegoiden kesken on turvallista. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miksi Bitwarden?: -Maailmanluokan salaus -Salasanat on suojattu tehokkaalla päästä päähän salauksella (AES-256 bit, suolattu hajautus ja PBKDF2 SHA-256), joten tietosi pysyvät turvassa ja yksityisinä. +More reasons to choose Bitwarden: -Sisäänrakennettu salasanageneraattori -Luo usein käyttämillesi sivustoille vahvoja, yksilöllisiä ja satunnaisia salasanoja. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Monikielinen -Bitwardenin sovelluksia on käännetty yli 40 kielelle ja määrä kasvaa jatkuvasti, kiitos kansainvälisen yhteisömme. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Alustariippumattomaton -Suojaa, käytä ja jaa Bitwarden-holvisi arkaluontoisia tietoja kaikilla selaimilla, mobiililaitteilla, pöytätietokoneilla ja muissa järjestelmissä. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Turvallinen ja ilmainen salasanahallinta kaikille laitteillesi + Kotona, töissä tai reissussa, Bitwarden suojaa helposti salasanasi, suojausavaimesi ja arkaluonteiset tietosi. Synkronoi ja hallitse holviasi useilla laitteilla diff --git a/apps/browser/store/locales/fil/copy.resx b/apps/browser/store/locales/fil/copy.resx index 4947fa6996..0f68a90bfa 100644 --- a/apps/browser/store/locales/fil/copy.resx +++ b/apps/browser/store/locales/fil/copy.resx @@ -1,17 +1,17 @@  - @@ -118,17 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Libreng Password Manager + Bitwarden Password Manager - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Si Bitwarden, Inc. ang parent company ng 8bit Solutions LLC. Tinawag na Pinakamahusay na Password Manager ng The Verge, U.S. News & World Report, CNET at iba pa. I-manage, i-store, i-secure at i-share ang walang limitasyong mga password sa walang limitasyong mga device mula sa kahit saan. Bitwarden nagbibigay ng mga open source na solusyon sa password management sa lahat, kahit sa bahay, sa trabaho o habang nasa daan. Lumikha ng mga matatas, natatanging, at mga random na password na naka-base sa mga pangangailangan ng seguridad para sa bawat website na madalas mong bisitahin. Ang Bitwarden Send ay nagpapadala ng maayos na naka-encrypt na impormasyon - mga file at plaintext - diretso sa sinuman. Ang Bitwarden ay nag-aalok ng mga Teams at Enterprise plans para sa m. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. + +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. + +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. + + +More reasons to choose Bitwarden: + +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. + +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Isang ligtas at libreng password manager para sa lahat ng iyong mga aparato. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. I-sync at i-access ang iyong kahadeyero mula sa maraming mga aparato diff --git a/apps/browser/store/locales/fr/copy.resx b/apps/browser/store/locales/fr/copy.resx index 9d311fe7cf..5c690049d0 100644 --- a/apps/browser/store/locales/fr/copy.resx +++ b/apps/browser/store/locales/fr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Gestion des mots de passe + Gestionnaire de mots de passe Bitwarden - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + À la maison, au travail ou en déplacement, Bitwarden sécurise facilement tous vos mots de passe, clés d'accès et informations sensibles. - Bitwarden, Inc. est la société mère de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMMÉ MEILLEUR GESTIONNAIRE DE MOTS DE PASSE PAR THE VERGE, U.S. NEWS & WORLD REPORT, CNET, ET PLUS ENCORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gérez, stockez, sécurisez et partagez un nombre illimité de mots de passe sur un nombre illimité d'appareils, où que vous soyez. Bitwarden fournit des solutions de gestion de mots de passe open source à tout le monde, que ce soit chez soi, au travail ou en déplacement. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Générez des mots de passe robustes, uniques et aléatoires basés sur des exigences de sécurité pour chaque site web que vous fréquentez. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmet rapidement des informations chiffrées --- fichiers et texte --- directement à quiconque. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden propose des plans Teams et Enterprise pour les sociétés afin que vous puissiez partager des mots de passe en toute sécurité avec vos collègues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Pourquoi choisir Bitwarden : -Un chiffrement de classe internationale -Les mots de passe sont protégés par un cryptage avancé de bout en bout (AES-256 bit, hachage salé et PBKDF2 SHA-256) afin que vos données restent sécurisées et privées. +More reasons to choose Bitwarden: -Générateur de mots de passe intégré -Générez des mots de passe forts, uniques et aléatoires en fonction des exigences de sécurité pour chaque site web que vous fréquentez. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traductions internationales -Les traductions de Bitwarden existent dans 40 langues et ne cessent de croître, grâce à notre communauté globale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Applications multiplateformes -Sécurisez et partagez des données sensibles dans votre coffre Bitwarden à partir de n'importe quel navigateur, appareil mobile ou système d'exploitation de bureau, et plus encore. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un gestionnaire de mots de passe sécurisé et gratuit pour tous vos appareils + À la maison, au travail ou en déplacement, Bitwarden sécurise facilement tous vos mots de passe, clés d'accès et informations sensibles. Synchroniser et accéder à votre coffre depuis plusieurs appareils diff --git a/apps/browser/store/locales/gl/copy.resx b/apps/browser/store/locales/gl/copy.resx index d812256fb7..0fdb224988 100644 --- a/apps/browser/store/locales/gl/copy.resx +++ b/apps/browser/store/locales/gl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,47 +118,64 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Xestor de contrasinais gratuíto + Bitwarden Password Manager - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. é a empresa matriz de 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADO MELLOR ADMINISTRADOR DE CONTRASINAIS POR THE VERGE, Ou.S. NEWS & WORLD REPORT, CNET E MÁS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Administre, almacene, protexa e comparta contrasinais ilimitados en dispositivos ilimitados desde calquera lugar. Bitwarden ofrece solucións de xestión de contrasinais de código aberto para todos, xa sexa en casa, no traballo ou en mentres estás de viaxe. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Xere contrasinais seguros, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapidamente información cifrada --- arquivos e texto sen formato, directamente a calquera persoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ofrece plans Teams e Enterprise para empresas para que poida compartir contrasinais de forma segura con colegas. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que elixir Bitwarden? -Cifrado de clase mundial -Os contrasinais están protexidas con cifrado avanzado de extremo a extremo (AES-256 bits, salted hashing e PBKDF2 XA-256) para que os seus datos permanezan seguros e privados. +More reasons to choose Bitwarden: -Xerador de contrasinais incorporado -Xere contrasinais fortes, únicas e aleatorias en función dos requisitos de seguridade de cada sitio web que frecuenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traducións Globais -As traducións de Bitwarden existen en 40 idiomas e están a crecer, grazas á nosa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicacións multiplataforma -Protexa e comparta datos confidenciais dentro da súa Caixa Forte de Bitwarden desde calquera navegador, dispositivo móbil ou sistema operativo de escritorio, e máis. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un xestor de contrasinais seguro e gratuíto para todos os teus dispositivos + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincroniza e accede á túa caixa forte desde múltiples dispositivos - Xestiona todos os teus usuarios e contrasinais desde unha caixa forte segura + Xestiona todos os teus inicios de sesión e contrasinais desde unha caixa forte segura Autocompleta rapidamente os teus datos de acceso en calquera páxina web que visites diff --git a/apps/browser/store/locales/he/copy.resx b/apps/browser/store/locales/he/copy.resx index cd980970fc..7f366f0e93 100644 --- a/apps/browser/store/locales/he/copy.resx +++ b/apps/browser/store/locales/he/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – מנהל ססמאות חינמי + Bitwarden Password Manager - מנהל ססמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - מנהל סיסמאות חינמי ומאובטח עבור כל המכשירים שלך + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. סנכרון וגישה לכספת שלך ממגוון מכשירים diff --git a/apps/browser/store/locales/hi/copy.resx b/apps/browser/store/locales/hi/copy.resx index 8db837a3c3..1ea7314d52 100644 --- a/apps/browser/store/locales/hi/copy.resx +++ b/apps/browser/store/locales/hi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - बिटवार्डन - मुक्त कूटशब्द प्रबंधक + Bitwarden Password Manager - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क कूटशब्द प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - आपके सभी उपकरणों के लिए एक सुरक्षित और नि: शुल्क पासवर्ड प्रबंधक + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. अनेक उपकरणों से अपने तिजोरी सिंक और एक्सेस करें diff --git a/apps/browser/store/locales/hr/copy.resx b/apps/browser/store/locales/hr/copy.resx index 5ff2bcbe01..dff95b3796 100644 --- a/apps/browser/store/locales/hr/copy.resx +++ b/apps/browser/store/locales/hr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - besplatni upravitelj lozinki + Bitwarden Password Manager - Siguran i besplatan upravitelj lozinki za sve vaše uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je vlasnik tvrtke 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET I DRUGI ODABRALI SU BITWARDEN NAJBOLJIM UPRAVITELJEM LOZINKI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, spremajte, osigurajte i dijelite neograničen broj lozinki na neograničenom broju uređaja bilo gdje. Bitwarden omogućuje upravljanje lozinkama, bazirano na otvorenom kodu, svima, bilo kod kuće, na poslu ili u pokretu. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svaku web stranicu koju često posjećujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send omoguzćuje jednostavno i brzo slanje šifriranih podataka --- datoteki ili teksta -- direktno, bilo kome. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden nudi Teams i Enterprise planove za tvrtke kako biste sigurno mogli dijeliti lozinke s kolegama na poslu. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zašto odabrati Bitwarden? -Svjetski priznata enkripcija -Lozinke su zaštićene naprednim end-to-end šifriranjem (AES-256 bit, salted hashtag i PBKDF2 SHA-256) kako bi vaši osobni podaci ostali sigurni i samo vaši. +More reasons to choose Bitwarden: -Ugrađen generator lozinki -Generirajte jake, jedinstvene i nasumične lozinke bazirane na sigurnosnim zahtjevima za svako web mjesto koje često posjećujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svjetski dostupan -Bitwarden je, zahvaljujući našoj globalnoj zajednici, dostupan na više od 40 jezika. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Podržani svi OS -Osigurajte i sigurno dijelite osjetljive podatke sadržane u vašem Bitwarden trezoru iz bilo kojeg preglednika, mobilnog uređaja ili stolnog računala s bilo kojim OS. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Siguran i besplatan upravitelj lozinki za sve tvoje uređaje + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkroniziraj i pristupi svojem trezoru s više uređaja diff --git a/apps/browser/store/locales/hu/copy.resx b/apps/browser/store/locales/hu/copy.resx index 0b3761a8ad..3e6b8e42d4 100644 --- a/apps/browser/store/locales/hu/copy.resx +++ b/apps/browser/store/locales/hu/copy.resx @@ -1,17 +1,17 @@  - @@ -118,36 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ingyenes jelszókezelő + Bitwarden Password Manager - Egy biztonságos és ingyenes jelszókezelő az összes eszközre. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - A Bitwarden, Inc. a 8bit Solutions LLC anyavállalata. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -A VERGE, A US NEWS & WORLD REPORT, a CNET ÉS MÁSOK LEGJOBB JELSZÓKEZELŐJE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Korlátlan számú jelszavak kezelése, tárolása, védelme és megosztása korlátlan eszközökön bárhonnan. A Bitwarden nyílt forráskódú jelszókezelési megoldásokat kínál mindenkinek, legyen az otthon, a munkahelyen vagy útközben. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Hozzunk létre erős, egyedi és véletlenszerű jelszavakat a biztonsági követelmények alapján minden webhelyre, amelyet gyakran látogatunk. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send gyorsan továbbítja a titkosított információkat-fájlokat és egyszerű szöveget közvetlenül bárkinek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -A Bitwarden csapatokat és vállalati terveket kínál a vállalatok számára, így biztonságosan megoszthatja jelszavait kollégáival. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Miért válasszuk a Bitwardent: -Világszínvonalú titkosítási jelszavak fejlett végpontok közötti titkosítással (AES-256 bit, titkosított hashtag és PBKDF2 SHA-256) védettek, így az adatok biztonságban és titokban maradnak. +More reasons to choose Bitwarden: -Beépített jelszógenerátor A biztonsági követelmények alapján erős, egyedi és véletlenszerű jelszavakat hozhat létre minden gyakran látogatott webhelyen. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globális fordítások +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -A Bitwarden fordítások 40 nyelven léteznek és globális közösségünknek köszönhetően egyre bővülnek. Többplatformos alkalmazások Biztonságos és megoszthatja az érzékeny adatokat a Bitwarden Széfben bármely böngészőből, mobileszközről vagy asztali operációs rendszerből stb. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Egy biztonságos és ingyenes jelszókezelő az összes eszközre + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. A széf szinkronizálása és elérése több eszközön. diff --git a/apps/browser/store/locales/id/copy.resx b/apps/browser/store/locales/id/copy.resx index b52252a342..b0791fa3b1 100644 --- a/apps/browser/store/locales/id/copy.resx +++ b/apps/browser/store/locales/id/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Pengelola Sandi Gratis + Bitwarden Password Manager - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Pengelola sandi yang aman dan gratis untuk semua perangkat Anda + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinkronkan dan akses brankas Anda dari beberapa perangkat diff --git a/apps/browser/store/locales/it/copy.resx b/apps/browser/store/locales/it/copy.resx index 56bf9a907c..cfa7111c3f 100644 --- a/apps/browser/store/locales/it/copy.resx +++ b/apps/browser/store/locales/it/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Password Manager Gratis + Bitwarden Password Manager - Un password manager sicuro e gratis per tutti i tuoi dispositivi + A casa, al lavoro, o in viaggio, Bitwarden protegge facilmente tutte le tue password, passkey, e informazioni sensibili. - Bitwarden, Inc. è la società madre di 8bit Solutions LLC. + Riconosciuto come il miglior password manager da PCMag, WIRED, The Verge, CNET, G2, e altri! -NOMINATO MIGLIOR PASSWORD MANAGER DA THE VERGE, U.S. NEWS & WORLD REPORT, CNET, E ALTRO. +PROTEGGI LA TUA VITA DIGITALE +Proteggi la tua vita digitale e proteggiti dalle violazioni dei dati generando e salvando password uniche e complesse per ogni account. Mantieni tutto in un archivio di password crittografato end-to-end a cui solo tu puoi accedere. -Gestisci, archivia, proteggi, e condividi password illimitate su dispositivi illimitati da qualsiasi luogo. Bitwarden offre soluzioni di gestione delle password open-source a tutti, a casa, al lavoro, o in viaggio. +ACCEDI AI TUOI DATI, OVUNQUE, IN QUALSIASI MOMENTO, SU QUALSIASI DISPOSITIVO +Gestisci, archivia, proteggi, e condividi facilmente password illimitate su un numero illimitato di dispositivi senza restrizioni. -Genera password forti, uniche, e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +TUTTI DOVREBBERO POTER RIMANERE AL SICURO ONLINE +Usa Bitwarden gratuitamente senza pubblicità o vendita di dati. Bitwarden ritiene che tutti dovrebbero avere la possibilità di rimanere al sicuro online. I piani premium offrono l'accesso a funzionalità avanzate. -Bitwarden Send trasmette rapidamente informazioni crittate - via file e testo in chiaro - direttamente a chiunque. +POTENZIA I TUOI TEAM CON BITWARDEN +I piani per Teams ed Enterprise includono funzionalità aziendali professionali. Alcuni esempi includono integrazione SSO, self-hosting, integrazione di directory e provisioning SCIM, politiche globali, accesso API, registri eventi, e altro. -Bitwarden offre piani Teams ed Enterprise per le aziende così puoi condividere le password in modo sicuro con i tuoi colleghi. +Utilizza Bitwarden per proteggere i tuoi dipendenti e condividere informazioni sensibili con i colleghi. -Perché Scegliere Bitwarden: -Crittografia Di Livello Mondiale -Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashing, e PBKDF2 SHA-256) per tenere i tuoi dati al sicuro e privati. +Altri motivi per scegliere Bitwarden: -Generatore Di Password Integrato -Genera password forti, uniche e casuali in base ai requisiti di sicurezza per ogni sito web che frequenti. +Crittografia di livello mondiale +Le password sono protette con crittografia end-to-end avanzata (AES-256 bit, salted hashtag, e PBKDF2 SHA-256) in modo che i tuoi dati rimangano sicuri e privati. -Traduzioni Globali -Le traduzioni di Bitwarden esistono in 40 lingue e sono in crescita grazie alla nostra comunità globale. +Controlli di terze parti +Bitwarden conduce regolarmente controlli di sicurezza completi di terze parti con importanti società di sicurezza. Questi controlli annuali includono valutazioni del codice sorgente e test di penetrazione su IP, server, e applicazioni web di Bitwarden. -Applicazioni Multipiattaforma -Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop, e altro. +2FA avanzato +Proteggi il tuo accesso con un autenticatore di terze parti, codici inviati via email, o credenziali FIDO2 WebAuthn come una chiave di sicurezza hardware o una passkey. + +Bitwarden Send +Trasmetti dati sensibili direttamente ad altri mantenendo la sicurezza crittografata end-to-end e limitando l'esposizione. + +Generatore incorporato +Crea password lunghe, complesse, e distinte e nomi utente univoci per ogni sito che visiti. Integrazione con fornitori di alias e-mail per una maggiore privacy. + +Traduzioni globali +Le traduzioni di Bitwarden esistono per più di 60 lingue, tradotte dalla comunità globale tramite Crowdin. + +App multipiattaforma +Proteggi e condividi i dati sensibili all'interno della tua cassaforte di Bitwarden da qualsiasi browser, dispositivo mobile, o sistema operativo desktop. + +Bitwarden protegge molto più che semplici password +Le soluzioni di gestione delle credenziali crittografate end-to-end di Bitwarden consentono alle organizzazioni di proteggere tutto, compresi i segreti degli sviluppatori ed esperienze con le passkey. Visita Bitwarden.com per saperne di più su Bitwarden Secrets Manager e Bitwarden Passwordless.dev! - Un password manager sicuro e gratis per tutti i tuoi dispositivi + A casa, al lavoro, o in viaggio, Bitwarden protegge facilmente tutte le tue password, passkey, e informazioni sensibili. Sincronizza e accedi alla tua cassaforte da più dispositivi diff --git a/apps/browser/store/locales/ja/copy.resx b/apps/browser/store/locales/ja/copy.resx index 13ce1bc4e9..67c479fcde 100644 --- a/apps/browser/store/locales/ja/copy.resx +++ b/apps/browser/store/locales/ja/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 無料パスワードマネージャー + Bitwarden Password Manager - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc.は8bit Solutions LLC.の親会社です。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGEやU.S. NEWS、WORLD REPORT、CNETなどからベストパスワードマネージャーに選ばれました。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -端末や場所を問わずパスワードの管理・保存・保護・共有を無制限にできます。Bitwardenは自宅や職場、外出先でもパスワード管理をすべての人に提供し、プログラムコードは公開されています。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Sendは、暗号化した情報(ファイルや平文)をすぐに誰にでも直接送信することができます。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwardenは企業向けにTeamsとEnterpriseのプランを提供しており、パスワードを同僚と安全に共有することができます。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwardenを選ぶ理由は? -・世界最高レベルの暗号化 -パスワードは高度なエンドツーエンド暗号化(AES-256 bit、salted hashing、PBKDF2 SHA-256)で保護されるので、データは安全に非公開で保たれます。 +More reasons to choose Bitwarden: -・パスワード生成機能 -よく利用するどのWebサイトでも、セキュリティ条件にそった強力でユニークなパスワードをランダムに生成することができます。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -・グローバルな翻訳 -Bitwardenは40ヶ国語に翻訳されており、グローバルなコミュニティのおかげで増え続けています。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -・クロスプラットフォームアプリケーション -あなたのBitwarden Vaultで、ブラウザ・モバイル機器・デスクトップOSなどの垣根を超えて、機密データを保護・共有することができます。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - あらゆる端末で使える、安全な無料パスワードマネージャー + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 複数の端末で保管庫に同期&アクセス diff --git a/apps/browser/store/locales/ka/copy.resx b/apps/browser/store/locales/ka/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ka/copy.resx +++ b/apps/browser/store/locales/ka/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/km/copy.resx b/apps/browser/store/locales/km/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/km/copy.resx +++ b/apps/browser/store/locales/km/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/kn/copy.resx b/apps/browser/store/locales/kn/copy.resx index 6928f557e4..f68f2c25da 100644 --- a/apps/browser/store/locales/kn/copy.resx +++ b/apps/browser/store/locales/kn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - ಬಿಟ್ವರ್ಡ್ – ಉಚಿತ ಪಾಸ್ವರ್ಡ್ ನಿರ್ವಾಹಕ + Bitwarden Password Manager - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - ಬಿಟ್ವಾರ್ಡೆನ್, ಇಂಕ್. 8 ಬಿಟ್ ಸೊಲ್ಯೂಷನ್ಸ್ ಎಲ್ಎಲ್ ಸಿ ಯ ಮೂಲ ಕಂಪನಿಯಾಗಿದೆ. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ವರ್ಜ್, ಯು.ಎಸ್. ನ್ಯೂಸ್ & ವರ್ಲ್ಡ್ ರಿಪೋರ್ಟ್, ಸಿನೆಟ್ ಮತ್ತು ಹೆಚ್ಚಿನದರಿಂದ ಉತ್ತಮ ಪಾಸ್‌ವರ್ಡ್ ವ್ಯವಸ್ಥಾಪಕ ಎಂದು ಹೆಸರಿಸಲಾಗಿದೆ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -ಎಲ್ಲಿಂದಲಾದರೂ ಅನಿಯಮಿತ ಸಾಧನಗಳಲ್ಲಿ ಅನಿಯಮಿತ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಿರ್ವಹಿಸಿ, ಸಂಗ್ರಹಿಸಿ, ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. ಮನೆಯಲ್ಲಿ, ಕೆಲಸದಲ್ಲಿ ಅಥವಾ ಪ್ರಯಾಣದಲ್ಲಿರಲಿ ಪ್ರತಿಯೊಬ್ಬರಿಗೂ ಬಿಟ್‌ವಾರ್ಡೆನ್ ಓಪನ್ ಸೋರ್ಸ್ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಹಣಾ ಪರಿಹಾರಗಳನ್ನು ನೀಡುತ್ತದೆ. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಳುಹಿಸಿ ಎನ್‌ಕ್ರಿಪ್ಟ್ ಮಾಡಿದ ಮಾಹಿತಿಯನ್ನು ತ್ವರಿತವಾಗಿ ರವಾನಿಸುತ್ತದೆ --- ಫೈಲ್‌ಗಳು ಮತ್ತು ಸರಳ ಪಠ್ಯ - ನೇರವಾಗಿ ಯಾರಿಗಾದರೂ. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಕಂಪೆನಿಗಳಿಗೆ ತಂಡಗಳು ಮತ್ತು ಎಂಟರ್‌ಪ್ರೈಸ್ ಯೋಜನೆಗಳನ್ನು ನೀಡುತ್ತದೆ ಆದ್ದರಿಂದ ನೀವು ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸಹೋದ್ಯೋಗಿಗಳೊಂದಿಗೆ ಸುರಕ್ಷಿತವಾಗಿ ಹಂಚಿಕೊಳ್ಳಬಹುದು. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ಬಿಟ್‌ವಾರ್ಡೆನ್ ಅನ್ನು ಏಕೆ ಆರಿಸಬೇಕು: -ವಿಶ್ವ ದರ್ಜೆಯ ಗೂ ry ಲಿಪೀಕರಣ -ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಸುಧಾರಿತ ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್ (ಎಇಎಸ್ -256 ಬಿಟ್, ಉಪ್ಪುಸಹಿತ ಹ್ಯಾಶ್‌ಟ್ಯಾಗ್ ಮತ್ತು ಪಿಬಿಕೆಡಿಎಫ್ 2 ಎಸ್‌ಎಚ್‌ಎ -256) ನೊಂದಿಗೆ ರಕ್ಷಿಸಲಾಗಿದೆ ಆದ್ದರಿಂದ ನಿಮ್ಮ ಡೇಟಾ ಸುರಕ್ಷಿತ ಮತ್ತು ಖಾಸಗಿಯಾಗಿರುತ್ತದೆ. +More reasons to choose Bitwarden: -ಅಂತರ್ನಿರ್ಮಿತ ಪಾಸ್ವರ್ಡ್ ಜನರೇಟರ್ -ನೀವು ಆಗಾಗ್ಗೆ ಪ್ರತಿ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಸುರಕ್ಷತಾ ಅವಶ್ಯಕತೆಗಳನ್ನು ಆಧರಿಸಿ ಬಲವಾದ, ಅನನ್ಯ ಮತ್ತು ಯಾದೃಚ್ pass ಿಕ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ರಚಿಸಿ. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -ಜಾಗತಿಕ ಅನುವಾದಗಳು -ಬಿಟ್ವಾರ್ಡೆನ್ ಅನುವಾದಗಳು 40 ಭಾಷೆಗಳಲ್ಲಿ ಅಸ್ತಿತ್ವದಲ್ಲಿವೆ ಮತ್ತು ಬೆಳೆಯುತ್ತಿವೆ, ನಮ್ಮ ಜಾಗತಿಕ ಸಮುದಾಯಕ್ಕೆ ಧನ್ಯವಾದಗಳು. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -ಕ್ರಾಸ್ ಪ್ಲಾಟ್‌ಫಾರ್ಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳು -ಯಾವುದೇ ಬ್ರೌಸರ್, ಮೊಬೈಲ್ ಸಾಧನ, ಅಥವಾ ಡೆಸ್ಕ್‌ಟಾಪ್ ಓಎಸ್ ಮತ್ತು ಹೆಚ್ಚಿನವುಗಳಿಂದ ನಿಮ್ಮ ಬಿಟ್‌ವಾರ್ಡನ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಸೂಕ್ಷ್ಮ ಡೇಟಾವನ್ನು ಸುರಕ್ಷಿತಗೊಳಿಸಿ ಮತ್ತು ಹಂಚಿಕೊಳ್ಳಿ. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - ನಿಮ್ಮ ಎಲ್ಲಾ ಸಾಧನಗಳಿಗೆ ಸುರಕ್ಷಿತ ಮತ್ತು ಉಚಿತ ಪಾಸ್‌ವರ್ಡ್ ನಿರ್ವಾಹಕ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ಅನೇಕ ಸಾಧನಗಳಿಂದ ನಿಮ್ಮ ವಾಲ್ಟ್ ಅನ್ನು ಸಿಂಕ್ ಮಾಡಿ ಮತ್ತು ಪ್ರವೇಶಿಸಿ diff --git a/apps/browser/store/locales/ko/copy.resx b/apps/browser/store/locales/ko/copy.resx index 0fb5dd713f..595663b1ca 100644 --- a/apps/browser/store/locales/ko/copy.resx +++ b/apps/browser/store/locales/ko/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - 무료 비밀번호 관리자 + Bitwarden Password Manager - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. - Bitwarden, Inc.은 8bit Solutions LLC.의 모회사입니다. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -VERGE, U.S. NEWS, WORLD REPORT, CNET 등에서 최고의 비밀번호 관리자라고 평가했습니다! +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -관리하고, 보관하고, 보호하고, 어디에서든 어떤 기기에서나 무제한으로 비밀번호를 공유하세요. Bitwarden은 모두에게 오픈소스 비밀번호 관리 솔루션을 제공합니다. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send는 빠르게 암호화된 파일과 텍스트를 모두에게 전송할 수 있습니다. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden은 회사들을 위해 팀과 기업 플랜을 제공해서 동료에게 안전하게 비밀번호를 공유할 수 있습니다. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Bitwarden을 선택하는 이유: -세계 최고의 암호화 -비밀번호는 고급 종단간 암호화 (AES-256 bit, salted hashtag, 그리고 PBKDF2 SHA-256)을 이용하여 보호되기 때문에 데이터를 안전하게 보관할 수 있습니다. +More reasons to choose Bitwarden: -내장 비밀번호 생성기 -강하고, 독특하고, 랜덤한 비밀번호를 모든 웹사이트의 보안 요구사항에 따라 생성할 수 있습니다. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -언어 지원 -Bitwarden 번역은 전 세계의 커뮤니티 덕분에 40개의 언어를 지원하고 더 성장하고 있습니다. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -크로스 플랫폼 애플리케이션 -Bitwarden 보관함에 있는 민감한 정보를 어떠한 브라우저, 모바일 기기, 데스크톱 OS 등을 이용하여 보호하고 공유하세요. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - 당신의 모든 기기에서 사용할 수 있는, 안전한 무료 비밀번호 관리자 + 집에서도, 직장에서도, 이동 중에도 Bitwarden은 비밀번호, 패스키, 민감 정보를 쉽게 보호합니다. 여러 기기에서 보관함에 접근하고 동기화할 수 있습니다. diff --git a/apps/browser/store/locales/lt/copy.resx b/apps/browser/store/locales/lt/copy.resx index 92009c5c6d..d83c6ca99a 100644 --- a/apps/browser/store/locales/lt/copy.resx +++ b/apps/browser/store/locales/lt/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – nemokamas slaptažodžių tvarkyklė + Bitwarden Password Manager - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. yra patronuojančioji 8bit Solutions LLC įmonė. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -GERIAUSIU SLAPTAŽODŽIŲ TVARKYTOJU PRIPAŽINTAS THE VERGE, U.S. NEWS & WORLD REPORT, CNET IR KT. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Tvarkyk, laikyk, saugok ir bendrink neribotą skaičių slaptažodžių neribotuose įrenginiuose iš bet kurios vietos. Bitwarden teikia atvirojo kodo slaptažodžių valdymo sprendimus visiems – tiek namuose, tiek darbe, ar keliaujant. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai lankomai svetainei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send greitai perduoda užšifruotą informaciją – failus ir paprastą tekstą – tiesiogiai bet kam. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden siūlo komandoms ir verslui planus įmonėms, kad galėtum saugiai dalytis slaptažodžiais su kolegomis. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kodėl rinktis Bitwarden: -Pasaulinės klasės šifravimas -Slaptažodžiai yra saugomi su pažangiu šifravimu nuo galo iki galo (AES-256 bitų, sūdytu šifravimu ir PBKDF2 SHA-256), todėl tavo duomenys išliks saugūs ir privatūs. +More reasons to choose Bitwarden: -Integruotas slaptažodžių generatorius -Generuok stiprius, unikalius ir atsitiktinius slaptažodžius pagal saugos reikalavimus kiekvienai dažnai lankomai svetainei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Visuotiniai vertimai -Mūsų pasaulinės bendruomenės dėka Bitwarden vertimai egzistuoja 40 kalbose ir vis daugėja. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Įvairių platformų programos -Apsaugok ir bendrink neskelbtinus duomenis savo Bitwarden Vault iš bet kurios naršyklės, mobiliojo įrenginio ar darbalaukio OS ir kt. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Saugi ir nemokama slaptažodžių tvarkyklė visiems įrenginiams + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Pasiekite savo saugyklą iš kelių įrenginių diff --git a/apps/browser/store/locales/lv/copy.resx b/apps/browser/store/locales/lv/copy.resx index aec5e836c1..e64cc2eb3a 100644 --- a/apps/browser/store/locales/lv/copy.resx +++ b/apps/browser/store/locales/lv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezmaksas Paroļu Pārvaldnieks + Bitwarden Password Manager - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. ir 8bit Solutions LLC mātesuzņēmums. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET UN CITI ATZINA PAR LABĀKO PAROĻU PĀRVALDNIEKU. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Pārvaldi, uzglabā, aizsargā un kopīgo neierobežotu skaitu paroļu neierobežotā skaitā ierīču no jebkuras vietas. Bitwarden piedāvā atvērtā koda paroļu pārvaldības risinājumus ikvienam - gan mājās, gan darbā, gan ceļā. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām, katrai bieži apmeklētai vietnei. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send ātri pārsūta šifrētu informāciju - failus un atklātu tekstu - tieši jebkuram. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden piedāvā Teams un Enterprise plānus uzņēmumiem, lai tu varētu droši kopīgot paroles ar kolēģiem. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Kāpēc izvēlēties Bitwarden: -Pasaules klases šifrēšana -Paroles tiek aizsargātas ar modernu end-to-end šifrēšanu (AES-256 bitu, sālītu šifrēšanu un PBKDF2 SHA-256), lai tavi dati paliktu droši un privāti. +More reasons to choose Bitwarden: -Iebūvēts paroļu ģenerators -Ģenerē spēcīgas, unikālas un nejaušas paroles, pamatojoties uz drošības prasībām katrai bieži apmeklētai vietnei. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globālie tulkojumi -Bitwarden tulkojumi ir pieejami 40 valodās, un to skaits turpina pieaugt, pateicoties mūsu globālajai kopienai. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Starpplatformu lietojumprogrammas -Nodrošini un kopīgo sensitīvus datus savā Bitwarden Seifā no jebkuras pārlūkprogrammas, mobilās ierīces vai darbvirsmas operētājsistēmas un daudz ko citu. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Drošs un bezmaksas paroļu pārvaldnieks priekš visām jūsu ierīcēm. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizē un piekļūsti savai glabātavai no vairākām ierīcēm diff --git a/apps/browser/store/locales/ml/copy.resx b/apps/browser/store/locales/ml/copy.resx index cf9b631227..e22993d5b7 100644 --- a/apps/browser/store/locales/ml/copy.resx +++ b/apps/browser/store/locales/ml/copy.resx @@ -1,17 +1,17 @@  - @@ -118,27 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - സൗജന്യ പാസ്സ്‌വേഡ് മാനേജർ + Bitwarden Password Manager - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും പാസ്‌വേഡുകളും സംഭരിക്കുന്നതിനുള്ള ഏറ്റവും എളുപ്പവും സുരക്ഷിതവുമായ മാർഗ്ഗമാണ് Bitwarden, ഒപ്പം നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളും തമ്മിൽ സമന്വയിപ്പിക്കുകയും ചെയ്യുന്നു. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -പാസ്‌വേഡ് മോഷണം ഗുരുതരമായ പ്രശ്‌നമാണ്. നിങ്ങൾ ഉപയോഗിക്കുന്ന വെബ്‌സൈറ്റുകളും അപ്ലിക്കേഷനുകളും എല്ലാ ദിവസവും ആക്രമണത്തിലാണ്. സുരക്ഷാ ലംഘനങ്ങൾ സംഭവിക്കുകയും നിങ്ങളുടെ പാസ്‌വേഡുകൾ മോഷ്‌ടിക്കപ്പെടുകയും ചെയ്യുന്നു. അപ്ലിക്കേഷനുകളിലും വെബ്‌സൈറ്റുകളിലും ഉടനീളം സമാന പാസ്‌വേഡുകൾ നിങ്ങൾ വീണ്ടും ഉപയോഗിക്കുമ്പോൾ ഹാക്കർമാർക്ക് നിങ്ങളുടെ ഇമെയിൽ, ബാങ്ക്, മറ്റ് പ്രധാനപ്പെട്ട അക്കൗണ്ടുകൾ എന്നിവ എളുപ്പത്തിൽ ആക്‌സസ്സുചെയ്യാനാകും. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങളിലും സമന്വയിപ്പിക്കുന്ന ഒരു എൻ‌ക്രിപ്റ്റ് ചെയ്ത വാൾട്ടിൽ Bitwarden നിങ്ങളുടെ എല്ലാ ലോഗിനുകളും സംഭരിക്കുന്നു. നിങ്ങളുടെ ഉപകരണം വിടുന്നതിനുമുമ്പ് ഇത് പൂർണ്ണമായും എൻ‌ക്രിപ്റ്റ് ചെയ്‌തിരിക്കുന്നതിനാൽ, നിങ്ങളുടെ ഡാറ്റ നിങ്ങൾക്ക് മാത്രമേ ആക്‌സസ് ചെയ്യാൻ കഴിയൂ . Bitwarden ടീമിന് പോലും നിങ്ങളുടെ ഡാറ്റ വായിക്കാൻ കഴിയില്ല. നിങ്ങളുടെ ഡാറ്റ AES-256 ബിറ്റ് എൻ‌ക്രിപ്ഷൻ, സാൾട്ടിങ് ഹാഷിംഗ്, PBKDF2 SHA-256 എന്നിവ ഉപയോഗിച്ച് അടച്ചിരിക്കുന്നു. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -100% ഓപ്പൺ സോഴ്‌സ് സോഫ്റ്റ്വെയറാണ് Bitwarden . Bitwarden സോഴ്‌സ് കോഡ് GitHub- ൽ ഹോസ്റ്റുചെയ്‌തിരിക്കുന്നു, മാത്രമല്ല എല്ലാവർക്കും ഇത് അവലോകനം ചെയ്യാനും ഓഡിറ്റുചെയ്യാനും ബിറ്റ് വാർഡൻ കോഡ്ബേസിലേക്ക് സംഭാവന ചെയ്യാനും സ്വാതന്ത്ര്യമുണ്ട്. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. + +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. + +Use Bitwarden to secure your workforce and share sensitive information with colleagues. +More reasons to choose Bitwarden: +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. + +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - നിങ്ങളുടെ എല്ലാ ഉപകരണങ്ങൾക്കും സുരക്ഷിതവും സൗജന്യവുമായ പാസ്‌വേഡ് മാനേജർ. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ഒന്നിലധികം ഉപകരണങ്ങളിൽ നിന്ന് നിങ്ങളുടെ വാൾട് സമന്വയിപ്പിച്ച് ആക്‌സസ്സുചെയ്യുക diff --git a/apps/browser/store/locales/mr/copy.resx b/apps/browser/store/locales/mr/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/mr/copy.resx +++ b/apps/browser/store/locales/mr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/my/copy.resx b/apps/browser/store/locales/my/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/my/copy.resx +++ b/apps/browser/store/locales/my/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nb/copy.resx b/apps/browser/store/locales/nb/copy.resx index 74a8558db6..26a09cc855 100644 --- a/apps/browser/store/locales/nb/copy.resx +++ b/apps/browser/store/locales/nb/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden — Fri passordbehandling + Bitwarden Password Manager - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En sikker og fri passordbehandler for alle dine PCer og mobiler + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkroniser og få tilgang til ditt hvelv fra alle dine enheter diff --git a/apps/browser/store/locales/ne/copy.resx b/apps/browser/store/locales/ne/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/ne/copy.resx +++ b/apps/browser/store/locales/ne/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/nl/copy.resx b/apps/browser/store/locales/nl/copy.resx index e0779ba777..44dd02b439 100644 --- a/apps/browser/store/locales/nl/copy.resx +++ b/apps/browser/store/locales/nl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis wachtwoordbeheer + Bitwarden Password Manager - Een veilige en gratis oplossing voor wachtwoordbeheer voor al je apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is het moederbedrijf van 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -BESTE WACHTWOORDBEHEERDER VOLGENS THE VERGE, U.S. NEWS & WORLD REPORT, CNET EN ANDEREN. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten, waar je ook bent. Bitwarden levert open source wachtwoordbeheeroplossingen voor iedereen, of dat nu thuis, op het werk of onderweg is. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send verzendt snel versleutelde informatie --- bestanden en platte tekst -- rechtstreeks naar iedereen. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden biedt Teams- en Enterprise-abonnementen voor bedrijven, zodat je veilig wachtwoorden kunt delen met collega's. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Waarom Bitwarden: -Versleuteling van wereldklasse -Wachtwoorden worden beschermd met geavanceerde end-to-end-codering (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat jouw gegevens veilig en privé blijven. +More reasons to choose Bitwarden: -Ingebouwde wachtwoordgenerator -Genereer sterke, unieke en willekeurige wachtwoorden op basis van beveiligingsvereisten voor elke website die je bezoekt. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Wereldwijde vertalingen -Bitwarden-vertalingen bestaan ​​in 40 talen en groeien dankzij onze wereldwijde community. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Platformoverschrijdende toepassingen -Beveilig en deel gevoelige gegevens binnen uw Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop-besturingssysteem, en meer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Een veilige en gratis oplossing voor wachtwoordbeheer voor al uw apparaten + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchroniseer en gebruik je kluis op meerdere apparaten diff --git a/apps/browser/store/locales/nn/copy.resx b/apps/browser/store/locales/nn/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/nn/copy.resx +++ b/apps/browser/store/locales/nn/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/or/copy.resx b/apps/browser/store/locales/or/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/or/copy.resx +++ b/apps/browser/store/locales/or/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/pl/copy.resx b/apps/browser/store/locales/pl/copy.resx index 5b3941cb7e..5641c68c48 100644 --- a/apps/browser/store/locales/pl/copy.resx +++ b/apps/browser/store/locales/pl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - darmowy menedżer haseł + Menedżer Haseł Bitwarden - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. - Bitwarden, Inc. jest macierzystą firmą 8bit Solutions LLC. + Uznany za najlepszego menedżera haseł przez PCMag, WIRED, The Verge, CNET, G2 i wielu innych! -NAZWANY NAJLEPSZYM MENEDŻEREM HASEŁ PRZEZ THE VERGE, US NEWS & WORLD REPORT, CNET I WIĘCEJ. +ZABEZPIECZ SWOJE CYFROWE ŻYCIE +Zabezpiecz swoje cyfrowe życie i chroń przed naruszeniami danych, generując i zapisując unikalne, silne hasła do każdego konta. Przechowuj wszystko w zaszyfrowanym end-to-end magazynie haseł, do którego tylko Ty masz dostęp. -Zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń z każdego miejsca. Bitwarden dostarcza rozwiązania do zarządzania hasłami z otwartym kodem źródłowym każdemu, niezależnie od tego, czy jest w domu, w pracy, czy w podróży. +DOSTĘP DO SWOICH DANYCH W KAŻDYM MIEJSCU, W DOWOLNYM CZASIE, NA KAŻDYM URZĄDZENIU +Z łatwością zarządzaj, przechowuj, zabezpieczaj i udostępniaj nieograniczoną liczbę haseł na nieograniczonej liczbie urządzeń. -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +KAŻDY POWINIEN POSIADAĆ NARZĘDZIA ABY ZACHOWAĆ BEZPIECZEŃSTWO W INTERNECIE +Korzystaj z Bitwarden za darmo, bez reklam i sprzedawania Twoich danych. Bitwarden wierzy, że każdy powinien mieć możliwość zachowania bezpieczeństwa w Internecie. Plany premium oferują dostęp do zaawansowanych funkcji. -Funkcja Bitwarden Send szybko przesyła zaszyfrowane informacje --- pliki i zwykły tekst -- bezpośrednio do każdego. +WZMOCNIJ SWOJE ZESPOŁY DZIĘKI BITWARDEN +Plany dla Zespołów i Enterprise oferują profesjonalne funkcje biznesowe. Na przykład obejmują integrację z SSO, własny hosting, integrację katalogów i udostępnianie SCIM, zasady globalne, dostęp do API, dzienniki zdarzeń i inne. -Bitwarden oferuje plany dla zespołów i firm, dzięki czemu możesz bezpiecznie udostępniać hasła współpracownikom. +Użyj Bitwarden, aby zabezpieczyć swoich pracowników i udostępniać poufne informacje współpracownikom. -Dlaczego warto wybrać Bitwarden: -Szyfrowanie światowej klasy -Hasła są chronione za pomocą zaawansowanego szyfrowania typu end-to-end (AES-256 bitów, dodatkowy ciąg zaburzający i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. +Więcej powodów, aby wybrać Bitwarden: -Wbudowany generator haseł -Generuj silne, unikalne i losowe hasła w oparciu o wymagania bezpieczeństwa dla każdej odwiedzanej strony. +Szyfrowanie na światowym poziomie +Hasła są chronione za pomocą zaawansowanego, kompleksowego szyfrowania (AES-256-bitowy, solony hashtag i PBKDF2 SHA-256), dzięki czemu Twoje dane pozostają bezpieczne i prywatne. -Przetłumaczone aplikacje -Tłumaczenia Bitwarden są dostępne w 40 językach i rosną dzięki naszej globalnej społeczności. +Audyty stron trzecich +Bitwarden regularnie przeprowadza kompleksowe audyty bezpieczeństwa stron trzecich we współpracy ze znanymi firmami security. Te coroczne audyty obejmują ocenę kodu źródłowego i testy penetracyjne adresów IP Bitwarden, serwerów i aplikacji internetowych. + +Zaawansowane 2FA +Zabezpiecz swój login za pomocą zewnętrznego narzędzia uwierzytelniającego, kodów przesłanych pocztą elektroniczną lub poświadczeń FIDO2 WebAuthn, takich jak sprzętowy klucz bezpieczeństwa lub hasło. + +Bitwarden Wyślij +Przesyłaj dane bezpośrednio do innych, zachowując kompleksowe szyfrowane bezpieczeństwo i ograniczając ryzyko. + +Wbudowany generator +Twórz długie, złożone i różne hasła oraz unikalne nazwy użytkowników dla każdej odwiedzanej witryny. Zintegruj się z dostawcami aliasów e-mail, aby uzyskać dodatkową prywatność. + +Tłumaczenia globalne +Istnieją tłumaczenia Bitwarden na ponad 60 języków, tłumaczone przez globalną społeczność za pośrednictwem Crowdin. Aplikacje wieloplatformowe -Zabezpiecz i udostępniaj poufne dane w swoim sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego, systemu operacyjnego i nie tylko. +Zabezpiecz i udostępniaj poufne dane w swoim Sejfie Bitwarden z dowolnej przeglądarki, urządzenia mobilnego lub systemu operacyjnego na komputerze stacjonarnym i nie tylko. + +Bitwarden zabezpiecza nie tylko hasła +Rozwiązania do zarządzania danymi zaszyfrownaymi end-to-end od firmy Bitwarden umożliwiają organizacjom zabezpieczanie wszystkiego, w tym tajemnic programistów i kluczy dostępu. Odwiedź Bitwarden.com, aby dowiedzieć się więcej o Mendżerze Sekretów Bitwarden i Bitwarden Passwordless.dev! + - Bezpieczny i darmowy menedżer haseł dla wszystkich Twoich urządzeń + W domu, w pracy, lub w ruchu, Bitwarden z łatwością zabezpiecza wszystkie Twoje hasła, passkeys i poufne informacje. Synchronizacja i dostęp do sejfu z różnych urządzeń diff --git a/apps/browser/store/locales/pt_BR/copy.resx b/apps/browser/store/locales/pt_BR/copy.resx index 48111fa814..067f9357b2 100644 --- a/apps/browser/store/locales/pt_BR/copy.resx +++ b/apps/browser/store/locales/pt_BR/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gerenciador de Senhas Gratuito + Gerenciador de Senhas Bitwarden - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. - Bitwarden, Inc. é a empresa matriz da 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NOMEADA MELHOR GERENCIADORA DE SENHAS PELA VERGE, U.S. NEWS & WORLD REPORT, CNET, E MUITO MAIS. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gerenciar, armazenar, proteger e compartilhar senhas ilimitadas através de dispositivos ilimitados de qualquer lugar. Bitwarden fornece soluções de gerenciamento de senhas de código aberto para todos, seja em casa, no trabalho ou em viagem. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Gere senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você frequenta. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -A Bitwarden Send transmite rapidamente informações criptografadas --- arquivos e texto em formato de placa -- diretamente para qualquer pessoa. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferece equipes e planos empresariais para empresas para que você possa compartilhar senhas com colegas com segurança. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Por que escolher Bitwarden: -Criptografia de Classe Mundial -As senhas são protegidas com criptografia avançada de ponta a ponta (AES-256 bit, salted hashing e PBKDF2 SHA-256) para que seus dados permaneçam seguros e privados. +More reasons to choose Bitwarden: -Gerador de senhas embutido -Gerar senhas fortes, únicas e aleatórias com base nos requisitos de segurança para cada site que você freqüenta. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduções globais -As traduções Bitwarden existem em 40 idiomas e estão crescendo, graças à nossa comunidade global. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicações multiplataforma -Proteja e compartilhe dados sensíveis dentro de seu Bitwarden Vault a partir de qualquer navegador, dispositivo móvel ou SO desktop, e muito mais. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Um gerenciador de senhas gratuito e seguro para todos os seus dispositivos + Em casa, no trabalho, ou em qualquer lugar, o Bitwarden protege facilmente todas as suas senhas, senhas e informações confidenciais. Sincronize e acesse o seu cofre através de múltiplos dispositivos diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx index 845a94a3ca..65b81e2d2d 100644 --- a/apps/browser/store/locales/pt_PT/copy.resx +++ b/apps/browser/store/locales/pt_PT/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gestor de Palavras-passe Gratuito + Bitwarden - Gestor de Palavras-passe - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + Em casa, no trabalho, em todo o lado, o Bitwarden protege facilmente todas as suas palavras-passe, chaves de acesso e informações sensíveis. - A Bitwarden, Inc. é a empresa-mãe da 8bit Solutions LLC. + Reconhecido como o melhor gestor de palavras-passe pela PCMag, WIRED, The Verge, CNET, G2 e muito mais! -NOMEADO O MELHOR GESTOR DE PALAVRAS-PASSE PELO THE VERGE, U.S. NEWS & WORLD REPORT, CNET E MUITO MAIS. +PROTEJA A SUA VIDA DIGITAL +Proteja a sua vida digital e proteja-se contra violações de dados, ao gerar e guardar palavras-passe únicas e fortes para cada conta. Guarde tudo num cofre de palavras-passe encriptadas ponto a ponto a que só você pode aceder. -Gerir, armazenar, proteger e partilhar palavras-passe ilimitadas em dispositivos ilimitados a partir de qualquer lugar. O Bitwarden fornece soluções de gestão de palavras-passe de código aberto para todos, seja em casa, no trabalho ou onde estiver. +ACEDA AOS SEUS DADOS, EM QUALQUER LUGAR, A QUALQUER HORA, EM QUALQUER DISPOSITIVO +Gerir, armazenar, proteger e partilhar facilmente palavras-passe ilimitadas em dispositivos ilimitados, sem restrições. -Gera palavras-passe fortes, únicas e aleatórias com base em requisitos de segurança para todos os sites que frequenta. +TODOS DEVEM TER AS FERRAMENTAS PARA SE MANTEREM SEGUROS ONLINE +Utilize o Bitwarden gratuitamente, sem anúncios ou venda de dados. O Bitwarden acredita que todos devem ter a capacidade de se manterem seguros online. Os planos Premium oferecem acesso a funcionalidades avançadas. -O Bitwarden Send transmite rapidamente informações encriptadas - ficheiros e texto simples - diretamente a qualquer pessoa. +CAPACITE AS SUAS EQUIPAS COM O BITWARDEN +Os planos Equipas e Empresarial vêm com funcionalidades profissionais de negócios. Alguns exemplos incluem a integração SSO, auto-hospedagem, integração de diretório e provisionamento SCIM, políticas globais, acesso à API, logs de eventos e muito mais. -O Bitwarden oferece os planos Equipas e Empresarial destinados a empresas, para que possa partilhar de forma segura as palavras-passe com os seus colegas. +Utilize o Bitwarden para proteger a sua equipa de trabalho e partilhar informações sensíveis com os colegas. -Razões para escolher o Bitwarden: + +Mais motivos para escolher o Bitwarden: Encriptação de classe mundial -As palavras-passe são protegidas com encriptação avançada de ponta a ponta (AES-256 bit, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. +As palavras-passe são protegidas com encriptação avançada ponto a ponto (AES-256 bits, salted hashtag e PBKDF2 SHA-256) para que os seus dados permaneçam seguros e privados. -Gerador de palavras-passe incorporado -Gera palavras-passe fortes, únicas e aleatórias com base nos requisitos de segurança para todos os sites que frequenta. +Auditorias de terceiros +O Bitwarden realiza regularmente auditorias abrangentes de segurança de terceiros com empresas de segurança notáveis. Estas auditorias anuais incluem avaliações de código-fonte e testes de penetração em IPs, servidores e aplicações Web do Bitwarden. -Traduções globais -O Bitwarden está traduzido em 40 idiomas e está a crescer, graças à nossa comunidade global. +2FA avançado +Proteja o seu início de sessão com um autenticador de terceiros, códigos enviados por e-mail ou credenciais FIDO2 WebAuthn, como uma chave de segurança de hardware ou uma chave de acesso. -Aplicações multiplataforma -Proteja e partilhe dados confidenciais no seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel ou sistema operativo de computador, e muito mais. +Bitwarden Send +Envie dados diretamente a outros, mantendo a segurança encriptada ponto a ponto e limitando a exposição. + +Gerador incorporado +Crie palavras-passe longas, complexas e distintas e nomes de utilizador únicos para cada site que visita. Integre-se com fornecedores de pseudónimos de e-mail para privacidade adicional. + +Traduções globais +Existem traduções do Bitwarden para mais de 60 idiomas, traduzidas pela comunidade global através do Crowdin. + +Aplicações multiplataforma +Proteja e partilhe dados confidenciais dentro do seu cofre Bitwarden a partir de qualquer navegador, dispositivo móvel, ou SO de computador, e muito mais. + +O Bitwarden protege mais do que apenas palavras-passe +As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden permitem que as organizações protejam tudo, incluindo segredos de programadores e experiências com chaves de acesso. Visite Bitwarden.com para saber mais sobre o Bitwarden - Gestor de Segredos e o Bitwarden Passwordless.dev! - Um gestor de palavras-passe seguro e gratuito para todos os seus dispositivos + Em casa, no trabalho, em todo o lado, o Bitwarden protege facilmente todas as suas palavras-passe, chaves de acesso e informações sensíveis. Sincronize e aceda ao seu cofre através de vários dispositivos diff --git a/apps/browser/store/locales/ro/copy.resx b/apps/browser/store/locales/ro/copy.resx index 0e12b289af..7b0070fad2 100644 --- a/apps/browser/store/locales/ro/copy.resx +++ b/apps/browser/store/locales/ro/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Manager de parole gratuit + Bitwarden Password Manager - Un manager de parole sigur și gratuit pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. este compania mamă a 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NUMIT CEL MAI BUN MANAGER DE PAROLE DE CĂTRE THE VERGE, U.S. NEWS & WORLD REPORT, CNET ȘI MULȚI ALȚII. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Gestionați, stocați, securizați și partajați un număr nelimitat de parole pe un număr nelimitat de dispozitive, de oriunde. Bitwarden oferă soluții open source de gestionare a parolelor pentru toată lumea, fie că se află acasă, la serviciu sau în mișcare. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generați parole puternice, unice și aleatorii, bazate pe cerințe de securitate pentru fiecare site web pe care îl frecventați. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send transmite rapid informații criptate --- fișiere și text simple -- direct către oricine. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden oferă planuri Teams și Enterprise pentru companii, astfel încât să puteți partaja în siguranță parolele cu colegii. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -De ce să alegeți Bitwarden: -Criptare de clasă mondială -Parolele sunt protejate cu criptare avansată end-to-end (AES-256 bit, salted hashing și PBKDF2 SHA-256), astfel încât datele dvs. să rămână sigure și private. +More reasons to choose Bitwarden: -Generator de parole încorporat -Generați parole puternice, unice și aleatorii, bazate pe cerințele de securitate pentru fiecare site web pe care îl frecventați. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Traduceri la nivel mondial -Bitwarden este deja tradus în 40 de limbi și numărul lor crește, datorită comunității noastre mondiale. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplicații multi-platformă -Protejați și partajați date sensibile în seiful Bitwarden de pe orice browser, dispozitiv mobil sau sistem de operare desktop și multe altele. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Un manager de parole sigur și gratuit, pentru toate dispozitivele dvs. + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sincronizează și accesează seiful dvs. de pe multiple dispozitive diff --git a/apps/browser/store/locales/ru/copy.resx b/apps/browser/store/locales/ru/copy.resx index 4e48ecbc88..212a899f76 100644 --- a/apps/browser/store/locales/ru/copy.resx +++ b/apps/browser/store/locales/ru/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – бесплатный менеджер паролей + Bitwarden Password Manager - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. является материнской компанией 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЗВАН ЛУЧШИМ ДИСПЕТЧЕРОМ ПАРОЛЕЙ VERGE, US NEWS & WORLD REPORT, CNET И МНОГИМИ ДРУГИМИ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управляйте, храните, защищайте и делитесь неограниченным количеством паролей на неограниченном количестве устройств из любого места. Bitwarden предоставляет решения с открытым исходным кодом по управлению паролями для всех, дома, на работе или в дороге. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send быстро передает зашифрованную информацию - файлы и простой текст - напрямую кому угодно. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden предлагает для компаний планы Teams и Enterprise, чтобы вы могли безопасно делиться паролями с коллегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Почему выбирают Bitwarden: -Шифрование мирового класса -Пароли защищены передовым сквозным шифрованием (AES-256 bit, соленый хэштег и PBKDF2 SHA-256), поэтому ваши данные остаются в безопасности и конфиденциальности. +More reasons to choose Bitwarden: -Встроенный генератор паролей -Создавайте надежные, уникальные и случайные пароли на основе требований безопасности для каждого посещаемого вами сайта. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. - Глобальные переводы - Переводы Bitwarden существуют на 40 языках и постоянно растут благодаря нашему глобальному сообществу. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. - Кросс-платформенные приложения - Защищайте и делитесь конфиденциальными данными в вашем Bitwarden Vault из любого браузера, мобильного устройства, настольной ОС и т. д. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Защищенный и бесплатный менеджер паролей для всех ваших устройств + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизация и доступ к хранилищу с нескольких устройств diff --git a/apps/browser/store/locales/si/copy.resx b/apps/browser/store/locales/si/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/si/copy.resx +++ b/apps/browser/store/locales/si/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/sk/copy.resx b/apps/browser/store/locales/sk/copy.resx index ba2a2a5a07..de7fa7dee3 100644 --- a/apps/browser/store/locales/sk/copy.resx +++ b/apps/browser/store/locales/sk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Bezplatný správca hesiel + Bitwarden Password Manager - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je materská spoločnosť spoločnosti 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -OHODNOTENÝ AKO NAJLEPŠÍ SPRÁVCA HESIEL V THE VERGE, U.S. NEWS & WORLD REPORT, CNET A ĎALŠÍMI. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Spravujte, ukladajte, zabezpečte a zdieľajte neobmedzený počet hesiel naprieč neobmedzeným počtom zariadení odkiaľkoľvek. Bitwarden ponúka open source riešenie na správu hesiel komukoľvek, kdekoľvek doma, v práci alebo na ceste. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send rýchlo prenesie šifrované informácie -- súbory a text -- priamo komukoľvek. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponúka Teams a Enterprise paušály pre firmy, aby ste mohli bezpečne zdieľať hesla s kolegami. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Prečo si vybrať Bitwarden: -Svetová trieda v šifrovaní -Heslá sú chránené pokročilým end-to-end šifrovaním (AES-256 bit, salted hash a PBKDF2 SHA-256), takže Vaše dáta zostanú bezpečné a súkromné. +More reasons to choose Bitwarden: -Vstavaný generátor hesiel -Vygenerujte si silné, unikátne a náhodné heslá podľa bezpečnostných požiadaviek na každej stránke, ktorú navštevujete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Svetová lokalizácia -Vďaka našej globálnej komunite má Bitwarden neustále rastúcu lokalizáciu už do 40 jazykov. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Aplikácie pre rôzne platformy -Zabezpečte a zdieľajte súkromné dáta prostredníctvom Bitwarden trezora z ktoréhokoľvek prehliadača, mobilného zariadenia, alebo stolného počítača a ďalších. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Bezpečný a bezplatný správca hesiel pre všetky vaše zariadenia + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synchronizujte a pristupujte k vášmu trezoru z viacerých zariadení diff --git a/apps/browser/store/locales/sl/copy.resx b/apps/browser/store/locales/sl/copy.resx index 83288e3872..80886de48a 100644 --- a/apps/browser/store/locales/sl/copy.resx +++ b/apps/browser/store/locales/sl/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - brezplačni upravljalnik gesel + Bitwarden Password Manager - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. je matično podjetje podjetja 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAJBOŠJI UPRAVLJALNIK GESEL PO MNEJU THE VERGE, U.S. NEWS & WORLD REPORT, CNET IN DRUGIH. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Upravljajte, shranjujte, varujte in delite neomejeno število gesel na neomejenem številu naprav, kjerkoli. Bitwarden ponuja odprtokodne rešitve za upravljanje gesel vsem, tako doma kot v službi ali na poti. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Ustvarite močna, edinstvena in naključna gesla, skladna z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Z Bitwarden Send hitro prenesite šifrirane informacije --- datoteke in navadno besedilo -- neposredno komurkoli. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden ponuja storitvi za organizacije Teams in Enterprise, s katerima lahko gesla varno delite s sodelavci. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Zakaj izbrati Bitwarden: -Vrhunsko šifriranje -Gesla so zaščitena z naprednim šifriranjem (AES-256, soljene hash-vrednosti in PBKDF2 SHA-256), tako da vaši podatki ostanejo varni in zasebni. +More reasons to choose Bitwarden: -Vgrajeni generator gesel -Ustvarite močna, edinstvena in naključna gesla v skladu z varnostnimi zahtevami za vsako spletno mesto, ki ga obiščete. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Prevodi za ves svet -Bitwarden je preveden že v 40 jezikov, naša globalna skupnost pa ves čas posodabljan in ustvarja nove prevede. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Deluje na vseh platformah -Varujte in delite svoje občutljive podatke znotraj vašega Bitwarden trezorja v katerem koli brskalniku, mobilni napravi, namiznem računalniku in drugje. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - Varen in brezplačen upravljalnik gesel za vse vaše naprave + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sinhronizirajte svoj trezor gesel in dostopajte do njega z več naprav diff --git a/apps/browser/store/locales/sr/copy.resx b/apps/browser/store/locales/sr/copy.resx index 9bfe799035..9c34d5812a 100644 --- a/apps/browser/store/locales/sr/copy.resx +++ b/apps/browser/store/locales/sr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Бесплатни Менаџер Лозинке + Bitwarden Password Manager - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. је матична компанија фирме 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -Именован као најбољи управљач лозинкама од стране новинских сајтова као што су THE VERGE, U.S. NEWS & WORLD REPORT, CNET, и других. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Управљајте, чувајте, обезбедите, и поделите неограничен број лозинки са неограниченог броја уређаја где год да се налазите. Bitwarden свима доноси решења за управљање лозинкама која су отвореног кода, било да сте код куће, на послу, или на путу. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send брзо преноси шифроване информације--- датотеке и обичан текст-- директно и свима. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden нуди планове за компаније и предузећа како бисте могли безбедно да делите лозинке са вашим колегама. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Зашто изабрати Bitwarden: -Шифровање светске класе -Лозинке су заштићене напредним шифровањем од једног до другог краја (AES-256 bit, salted hashing, и PBKDF2 SHA-256) како би ваши подаци остали безбедни и приватни. +More reasons to choose Bitwarden: -Уграђен генератор лозинки -Генеришите јаке, јединствене, и насумичне лозинке у зависности од безбедносних захтева за сваки сајт који често посећујете. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Глобално преведен -Bitwarden преводи постоје за 40 језика и стално се унапређују, захваљујући нашој глобалној заједници. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Вишеплатформне апликације -Обезбедите и поделите осетљиве податке у вашем Bitwarden сефу из било ког претраживача, мобилног уређаја, или desktop оперативног система, и других. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Сигурни и бесплатни менаџер лозинке за сва Ваша уређаја + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронизујте и приступите сефу са више уређаја diff --git a/apps/browser/store/locales/sv/copy.resx b/apps/browser/store/locales/sv/copy.resx index 8b3cb2a402..6406ab013e 100644 --- a/apps/browser/store/locales/sv/copy.resx +++ b/apps/browser/store/locales/sv/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Gratis lösenordshanterare + Bitwarden Password Manager - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. är moderbolag till 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -UTNÄMND TILL DEN BÄSTA LÖSENORDSHANTERAREN AV THE VERGE, U.S. NEWS & WORLD REPORT, CNET MED FLERA. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Hantera, lagra, säkra och dela ett obegränsat antal lösenord mellan ett obegränsat antal enheter var som helst ifrån. Bitwarden levererar lösningar för lösenordshantering med öppen källkod till alla, vare sig det är hemma, på jobbet eller på språng. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send överför snabbt krypterad information --- filer och klartext -- direkt till vem som helst. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden erbjuder abonnemang för team och företag så att du säkert kan dela lösenord med kollegor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Varför välja Bitwarden: -Kryptering i världsklass -Lösenord skyddas med avancerad end-to-end-kryptering (AES-256 bitar, saltad hashtag och PBKDF2 SHA-256) så att dina data förblir säkra och privata. +More reasons to choose Bitwarden: -Inbyggd lösenordsgenerator -Generera starka, unika och slumpmässiga lösenord baserat på säkerhetskrav för varje webbplats du besöker. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Globala översättningar -Översättningar av Bitwarden finns på 40 språk och antalet växer tack vare vår globala gemenskap. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Plattformsoberoende program -Säkra och dela känsliga data i ditt Bitwardenvalv från alla webbläsare, mobiler och datorer. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - En säker och gratis lösenordshanterare för alla dina enheter + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Synkronisera och kom åt ditt valv från flera enheter diff --git a/apps/browser/store/locales/te/copy.resx b/apps/browser/store/locales/te/copy.resx index 191198691d..82e4eb1d88 100644 --- a/apps/browser/store/locales/te/copy.resx +++ b/apps/browser/store/locales/te/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Free Password Manager + Bitwarden Password Manager - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. is the parent company of 8bit Solutions LLC. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -NAMED BEST PASSWORD MANAGER BY THE VERGE, U.S. NEWS & WORLD REPORT, CNET, AND MORE. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Manage, store, secure, and share unlimited passwords across unlimited devices from anywhere. Bitwarden delivers open source password management solutions to everyone, whether at home, at work, or on the go. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send quickly transmits encrypted information --- files and plaintext -- directly to anyone. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden offers Teams and Enterprise plans for companies so you can securely share passwords with colleagues. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Why Choose Bitwarden: + +More reasons to choose Bitwarden: World-Class Encryption -Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashing, and PBKDF2 SHA-256) so your data stays secure and private. +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Built-in Password Generator -Generate strong, unique, and random passwords based on security requirements for every website you frequent. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Global Translations -Bitwarden translations exist in 40 languages and are growing, thanks to our global community. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. -Cross-Platform Applications +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - A secure and free password manager for all of your devices + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Sync and access your vault from multiple devices diff --git a/apps/browser/store/locales/th/copy.resx b/apps/browser/store/locales/th/copy.resx index 9c8965b01f..f784b1884b 100644 --- a/apps/browser/store/locales/th/copy.resx +++ b/apps/browser/store/locales/th/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – โปรแกรมจัดการรหัสผ่านฟรี + Bitwarden Password Manager - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. เป็นบริษัทแม่ของ 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ได้รับการระบุชื่อเป็น โปรแกรมจัดการรหัสผ่านที่ดีที่สุด โดย The Verge, U.S. News & World Report, CNET, และที่อื่นๆ +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -สามารถจัดการ จัดเก็บ ปกป้อง และแชร์รหัสผ่านไม่จำกัดจำนวนระหว่างอุปกรณ์ต่างๆ โดยไม่จำกัดจำนวนจากที่ไหนก็ได้ Bitwarden เสนอโซลูชันจัดการรหัสผ่านโอเพนซอร์สให้กับทุกคน ไม่ว่าจะอยู่ที่บ้าน ที่ทำงาน หรือนอกสถานที่ +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -สามารถส่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send สามารถส่งข้อมูลที่ถูกเข้ารหัส --- ไฟล์ หรือ ข้อความ -- ตรงไปยังใครก็ได้ได้อย่างรวดเร็ว +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden มีแผนแบบ Teams และ Enterprise สำหรับบริษัทต่างๆ ซึางคุณสามารถแชร์รหัสผ่านกับเพื่อนร่วมงานได้อย่างปลอดภัย +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -ทำไมควรเลือก Bitwarden: -การเข้ารหัสมาตรฐานโลก -รหัสผ่านจะได้รับการปกป้องด้วยการเข้ารหัสชั้นสูง (AES-256 บิต, salted hashtag, และ PBKDF2 SHA-256) แบบต้นทางถึงปลายทาง เพื่อให้ข้อมูลของคุณปลอดภัยและเป็นส่วนตัว +More reasons to choose Bitwarden: -มีตัวช่วยส่มสร้างรหัสผ่าน -สามารถสุ่มสร้างรหัสผ่านที่ปลอดภัยและไม่ซ้ำกัน ตามเงื่อนไขความปลอดภัยที่กำหนดได้ สำหรับเว็บไซต์ทุกแห่งที่คุณใช้งานบ่อย +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -แปลเป็นภาษาต่างๆ ทั่วโลก -Bitwarden ได้รับการแปลเป็นภาษาต่างๆ กว่า 40 ภาษา และกำลังเพิ่มขึ้นเรื่อยๆ ด้วยความสนับสนุนจากชุมชนผู้ใช้งานทั่วโลก +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -แอปพลิเคชันข้ามแพลตฟอร์ม -ปกป้องและแชร์ข้อมูลอ่อนไหวในตู้เซฟ Bitwarden จากเว็บเบราว์เซอร์ อุปกรณ์มือถือ หรือเดสท็อป หรือช่องทางอื่นๆ +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - โปรแกรมจัดการรหัสผ่านที่ปลอดภัยและฟรี สำหรับอุปกรณ์ทั้งหมดของคุณ + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. ซิงค์และเข้าถึงตู้นิรภัยของคุณจากหลายอุปกรณ์ diff --git a/apps/browser/store/locales/tr/copy.resx b/apps/browser/store/locales/tr/copy.resx index 1fc3e2a34b..fa53d09ee1 100644 --- a/apps/browser/store/locales/tr/copy.resx +++ b/apps/browser/store/locales/tr/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Ücretsiz Parola Yöneticisi + Bitwarden Parola Yöneticisi - Tüm aygıtlarınız için güvenli ve ücretsiz bir parola yöneticisi + İster evde ister işte veya yolda olun; Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar. - Bitwarden, Inc., 8bit Solutions LLC’nin ana şirketidir. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -THE VERGE, U.S. NEWS & WORLD REPORT, CNET VE BİRÇOK MEDYA KURULUŞUNA GÖRE EN İYİ PAROLA YÖNETİCİSİ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Sınırsız sayıda parolayı istediğiniz kadar cihazda yönetin, saklayın, koruyun ve paylaşın. Bitwarden; herkesin evde, işte veya yolda kullanabileceği açık kaynaklı parola yönetim çözümleri sunuyor. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send, şifrelenmiş bilgileri (dosyalar ve düz metinler) herkese hızlı bir şekilde iletmenizi sağlıyor. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden, parolaları iş arkadaşlarınızla güvenli bir şekilde paylaşabilmeniz için şirketlere yönelik Teams ve Enterprise paketleri de sunuyor. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Neden Bitwarden? -Üst düzey şifreleme -Parolalarınız gelişmiş uçtan uca şifreleme (AES-256 bit, salted hashing ve PBKDF2 SHA-256) ile korunuyor, böylece verileriniz güvende ve gizli kalıyor. +More reasons to choose Bitwarden: -Dahili parola oluşturucu -Sık kullandığınız web siteleri için güvenlik gereksinimlerinize uygun, güçlü, benzersiz ve rastgele parolalar oluşturabilirsiniz. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Çeviriler -Bitwarden 40 dilde kullanılabiliyor ve gönüllü topluluğumuz sayesinde çeviri sayısı giderek artıyor. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Her platformla uyumlu uygulamalar -Bitwarden kasanızdaki hassas verilere her tarayıcıdan, mobil cihazdan veya masaüstü işletim sisteminden ulaşabilir ve onları paylaşabilirsiniz. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Tüm cihazarınız için güvenli ve ücretsiz bir parola yöneticisi + İster evde ister işte veya yolda olun; Bitwarden tüm parolalarınızı, geçiş anahtarlarınızı ve hassas bilgilerinizi güvenle saklar. Hesabınızı senkronize ederek kasanıza tüm cihazlarınızdan ulaşın diff --git a/apps/browser/store/locales/uk/copy.resx b/apps/browser/store/locales/uk/copy.resx index d59cd7f103..5a7de18363 100644 --- a/apps/browser/store/locales/uk/copy.resx +++ b/apps/browser/store/locales/uk/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – Безплатний менеджер паролів + Bitwarden Password Manager - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - 8bit Solutions LLC є дочірньою компанією Bitwarden, Inc. + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -НАЙКРАЩИЙ МЕНЕДЖЕР ПАРОЛІВ ЗА ВЕРСІЄЮ THE VERGE, U.S. NEWS & WORLD REPORT, CNET, А ТАКОЖ ІНШИХ ВИДАНЬ. +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Зберігайте, захищайте, керуйте і надавайте доступ до паролів на різних пристроях де завгодно. Bitwarden пропонує рішення для керування паролями на основі відкритого програмного коду особистим та корпоративним користувачам на всіх пристроях. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Швидко відправляйте будь-кому зашифровану інформацію, як-от файли чи звичайний текст, за допомогою функції Bitwarden Send. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden пропонує командні та корпоративні тарифні плани для компаній, щоб ви могли безпечно обмінюватися паролями з колегами. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Чому варто обрати Bitwarden: -Всесвітньо визнані стандарти шифрування -Паролі захищаються з використанням розширеного наскрізного шифрування (AES-256 bit, хешування з сіллю та PBKDF2 SHA-256), тому ваші дані завжди захищені та приватні. +More reasons to choose Bitwarden: -Вбудований генератор паролів -Генеруйте надійні, випадкові та унікальні паролі, які відповідають вимогам безпеки, для кожного вебсайту та сервісу. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Переклад багатьма мовами -Завдяки нашій глобальній спільноті, Bitwarden перекладено 40 мовами, і їх кількість зростає. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Програми для різних платформ -Зберігайте і діліться важливими даними, а також користуйтеся іншими можливостями у вашому сховищі Bitwarden в будь-якому браузері, мобільному пристрої, чи комп'ютерній операційній системі. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Захищений, безплатний менеджер паролів для всіх ваших пристроїв + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Синхронізуйте й отримуйте доступ до свого сховища на різних пристроях diff --git a/apps/browser/store/locales/vi/copy.resx b/apps/browser/store/locales/vi/copy.resx index 220d50bdfa..e0403d1f32 100644 --- a/apps/browser/store/locales/vi/copy.resx +++ b/apps/browser/store/locales/vi/copy.resx @@ -1,17 +1,17 @@  - @@ -118,41 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden - Trình quản lý mật khẩu miễn phí + Bitwarden Password Manager - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc là công ty mẹ của 8bit Solutions LLC + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -ĐƯỢC ĐÁNH GIÁ LÀ TRÌNH QUẢN LÝ MẬT KHẨU TỐT NHẤT BỞI NHÀ BÁO LỚN NHƯ THE VERGE, CNET, U.S. NEWS & WORLD REPORT VÀ HƠN NỮA +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -Quản lý, lưu trữ, bảo mật và chia sẻ mật khẩu không giới hạn trên các thiết bị không giới hạn mọi lúc, mọi nơi. Bitwarden cung cấp các giải pháp quản lý mật khẩu mã nguồn mở cho tất cả mọi người, cho dù ở nhà, tại cơ quan hay khi đang di chuyển. +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -Tạo mật khẩu mạnh, không bị trùng và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web bạn thường xuyên sử dụng. +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Tính năng 'Bitwarden Send' nhanh chóng truyền thông tin được mã hóa --- tệp và văn bản - trực tiếp đến bất kỳ ai. +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden cung cấp các gói 'Nhóm' và 'Doanh nghiệp' cho các công ty để bạn có thể chia sẻ mật khẩu với đồng nghiệp một cách an toàn. +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -Tại sao bạn nên chọn Bitwarden: -Mã hóa tốt nhất thế giới -Mật khẩu được bảo vệ bằng mã hóa đầu cuối (end-to-end encryption) tiên tiến như AES-256 bit, salted hashtag, và PBKDF2 SHA-256 nên dữ liệu của bạn luôn an toàn và riêng tư. +More reasons to choose Bitwarden: -Trình tạo mật khẩu tích hợp -Tạo mật khẩu mạnh, không bị trùng lặp, và ngẫu nhiên dựa trên các yêu cầu bảo mật cho mọi trang web mà bạn thường xuyên sử dụng. +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -Bản dịch ngôn ngữ từ cộng đồng -Bitwarden đã có bản dịch 40 ngôn ngữ và đang phát triển nhờ vào cộng đồng toàn cầu của chúng tôi. +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -Ứng dụng đa nền tảng -Bảo mật và chia sẻ dữ liệu nhạy cảm trong kho lưu trữ Bitwarden của bạn từ bất kỳ trình duyệt, điện thoại thông minh hoặc hệ điều hành máy tính nào, và hơn thế nữa. +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! - Một trình quản lý mật khẩu an toàn và miễn phí cho mọi thiết bị của bạn + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. Đồng bộ hóa và truy cập vào kho lưu trữ của bạn từ nhiều thiết bị diff --git a/apps/browser/store/locales/zh_CN/copy.resx b/apps/browser/store/locales/zh_CN/copy.resx index e424ef743a..d010cb1a7b 100644 --- a/apps/browser/store/locales/zh_CN/copy.resx +++ b/apps/browser/store/locales/zh_CN/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,56 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免费密码管理器 + Bitwarden 密码管理器 - 安全免费的跨平台密码管理器 + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + 被 PCMag、WIRED、The Verge、CNET、G2 等评为最佳密码管理器! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等评为最佳的密码管理器。 +保护您的数字生活 +通过为每个账户生成并保存独特而强大的密码,保护您的数字生活并防范数据泄露。所有内容保存在只有您可以访问的端对端加密的密码库中。 -从任何地方,不限制设备,管理、存储、保护和共享无限的密码。Bitwarden 为每个人提供开源的密码管理解决方案,无论是在家里,在工作中,还是在旅途中。 +随时随地在任何设备上访问您的数据 +不受任何限制跨无限数量的设备轻松管理、存储、保护和分享不限数量的密码。 -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +每个人都应该拥有的保持在线安全的工具 +使用 Bitwarden 是免费的,没有广告,不会出售数据。Bitwarden 相信每个人都应该拥有保持在线安全的能力。高级计划提供了堆高级功能的访问。 -Bitwarden Send 快速传输加密的信息---文件和文本---直接给任何人。 +通过 BITWARDEN 为您的团队提供支持 +团队和企业计划具有专业的商业功能。例如 SSO 集成、自托管、目录集成和 SCIM 配置、全局策略、API 访问、事件日志等。 -Bitwarden 为公司提供团队和企业计划,因此您可以安全地与同事共享密码。 +使用 Bitwarden 保护您的团队,并与同事共享敏感信息。 -为何选择 Bitwarden: +选择 Bitwarden 的更多理由: -世界级的加密技术 -密码受到先进的端到端加密(AES-256 位、盐化标签和 PBKDF2 SHA-256)的保护,为您的数据保持安全和隐密。 +世界级加密 +密码受到先进的端对端加密(AES-256 位、加盐哈希标签和 PBKDF2 SHA-256)保护,使您的数据保持安全和私密。 -内置密码生成器 -基于安全要求,为您经常访问的每个网站生成强大、唯一和随机的密码。 +第三方审计 +Bitwarden 定期与知名的安全公司进行全面的第三方安全审计。这些年度审核包括对 Bitwarden IP、服务器和 Web 应用程序的源代码评估和渗透测试。 -全球翻译 -Bitwarden 的翻译有 40 种语言,而且还在不断增加,感谢我们的全球社区。 +高级两步验证 +使用第三方身份验证器、通过电子邮件发送代码或 FIDO2 WebAuthn 凭据(如硬件安全钥匙或通行密钥)保护您的登录。 -跨平台应用程序 -从任何浏览器、移动设备或桌面操作系统,以及更多的地方,在您的 Bitwarden 密码库中保护和分享敏感数据。 +Bitwarden Send +直接传输数据给他人,同时保持端对端加密的安全性并防止曝露。 + +内置生成器 +为您访问的每个网站创建长、复杂且独特的密码和用户名。与电子邮件别名提供商集成,增加隐私保护。 + +全球翻译 +Bitwarden 的翻译涵盖 60 多种语言,由全球社区通过 Crowdin 翻译。 + +跨平台应用程序 +从任何浏览器、移动设备或桌面操作系统中安全地访问和共享 Bitwarden 密码库中的敏感数据。 + +Bitwarden 保护的不仅仅是密码 +Bitwarden 的端对端加密凭据管理解决方案使组织能够保护所有内容,包括开发人员机密和通行密钥体验。访问 Bitwarden.com 了解更多关于Bitwarden Secrets Manager 和 Bitwarden Passwordless.dev 的信息! - 安全免费的跨平台密码管理器 + 无论是在家里、工作中还是在外出时,Bitwarden 都可以轻松地保护您的所有密码、通行密钥和敏感信息。 从多台设备同步和访问密码库 diff --git a/apps/browser/store/locales/zh_TW/copy.resx b/apps/browser/store/locales/zh_TW/copy.resx index be39fdca06..ab37ed5f7b 100644 --- a/apps/browser/store/locales/zh_TW/copy.resx +++ b/apps/browser/store/locales/zh_TW/copy.resx @@ -1,17 +1,17 @@  - @@ -118,40 +118,58 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Bitwarden – 免費密碼管理工具 + Bitwarden Password Manager - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. - Bitwarden, Inc. 是 8bit Solutions LLC 的母公司。 + Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! -被 THE VERGE、U.S. NEWS & WORLD REPORT、CNET 等評為最佳的密碼管理器。 +SECURE YOUR DIGITAL LIFE +Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access. -從任何地方,不限制設備,管理、存儲、保護和共享無限的密碼。Bitwarden 為每個人提供開源的密碼管理解決方案,無論是在家裡,在工作中,還是在旅途中。 +ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE +Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE +Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features. -Bitwarden Send 快速傳輸加密的信息---文檔和文本---直接給任何人。 +EMPOWER YOUR TEAMS WITH BITWARDEN +Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more. -Bitwarden 為公司提供團隊和企業計劃,因此您可以安全地與同事共享密碼。 +Use Bitwarden to secure your workforce and share sensitive information with colleagues. -為何選擇 Bitwarden: -世界級的加密技術 -密碼受到先進的端到端加密(AES-256 位、鹽化標籤和 PBKDF2 SHA-256)的保護,為您的資料保持安全和隱密。 +More reasons to choose Bitwarden: -內置密碼生成器 -基於安全要求,為您經常訪問的每個網站生成強大、唯一和隨機的密碼。 +World-Class Encryption +Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private. -全球翻譯 -Bitwarden 的翻譯有 40 種語言,而且還在不斷增加,感謝我們的全球社區。 +3rd-party Audits +Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications. -跨平台應用程式 -從任何瀏覽器、行動裝置或桌面作業系統,以及更多的地方,在您的 Bitwarden 密碼庫中保護和分享敏感資料。 +Advanced 2FA +Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey. + +Bitwarden Send +Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. + +Built-in Generator +Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy. + +Global Translations +Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. + +Cross-Platform Applications +Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. + +Bitwarden secures more than just passwords +End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev! + - 安全、免費、跨平台的密碼管理工具 + At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information. 在多部裝置上同步和存取密碼庫 diff --git a/apps/browser/store/windows/AppxManifest.xml b/apps/browser/store/windows/AppxManifest.xml index f57b3db988..df02ea085c 100644 --- a/apps/browser/store/windows/AppxManifest.xml +++ b/apps/browser/store/windows/AppxManifest.xml @@ -11,7 +11,7 @@ Version="0.0.0.0"/> - Bitwarden Extension - Free Password Manager + Bitwarden Password Manager 8bit Solutions LLC Assets/icon_50.png @@ -30,10 +30,10 @@ @@ -41,7 +41,7 @@ + DisplayName="Bitwarden Password Manager"> diff --git a/apps/browser/test.setup.ts b/apps/browser/test.setup.ts index 1031268186..4800b4c17f 100644 --- a/apps/browser/test.setup.ts +++ b/apps/browser/test.setup.ts @@ -68,6 +68,8 @@ const tabs = { const scripting = { executeScript: jest.fn(), + registerContentScripts: jest.fn(), + unregisterContentScripts: jest.fn(), }; const windows = { @@ -124,6 +126,19 @@ const offscreen = { }, }; +const permissions = { + contains: jest.fn((permissions, callback) => { + callback(true); + }), +}; + +const webNavigation = { + onCommitted: { + addListener: jest.fn(), + removeListener: jest.fn(), + }, +}; + // set chrome global.chrome = { i18n, @@ -137,4 +152,6 @@ global.chrome = { privacy, extension, offscreen, + permissions, + webNavigation, } as any; diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 694246f59a..e1bf2b7211 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "moduleResolution": "node", "noImplicitAny": true, + "allowSyntheticDefaultImports": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "module": "ES2020", @@ -9,6 +10,7 @@ "allowJs": true, "sourceMap": true, "baseUrl": ".", + "lib": ["ES2021.String"], "paths": { "@bitwarden/admin-console": ["../../libs/admin-console/src"], "@bitwarden/angular/*": ["../../libs/angular/src/*"], diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js index 3b5724b198..2756ab4395 100644 --- a/apps/browser/webpack.config.js +++ b/apps/browser/webpack.config.js @@ -166,8 +166,6 @@ const mainConfig = { "content/notificationBar": "./src/autofill/content/notification-bar.ts", "content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts", "content/content-message-handler": "./src/autofill/content/content-message-handler.ts", - "content/fido2/trigger-fido2-content-script-injection": - "./src/vault/fido2/content/trigger-fido2-content-script-injection.ts", "content/fido2/content-script": "./src/vault/fido2/content/content-script.ts", "content/fido2/page-script": "./src/vault/fido2/content/page-script.ts", "notification/bar": "./src/autofill/notification/bar.ts", @@ -277,6 +275,8 @@ if (manifestVersion == 2) { mainConfig.entry.background = "./src/platform/background.ts"; mainConfig.entry["content/lp-suppress-import-download-script-append-mv2"] = "./src/tools/content/lp-suppress-import-download-script-append.mv2.ts"; + mainConfig.entry["content/fido2/page-script-append-mv2"] = + "./src/vault/fido2/content/page-script-append.mv2.ts"; configs.push(mainConfig); } else { diff --git a/apps/cli/package.json b/apps/cli/package.json index 690842d831..b57b818c63 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/cli", "description": "A secure and free password manager for all of your devices.", - "version": "2024.3.1", + "version": "2024.5.0", "keywords": [ "bitwarden", "password", @@ -71,7 +71,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.16", + "tldts": "6.1.18", "zxcvbn": "4.4.2" } } diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index a91e876e92..bd61727a6c 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -16,6 +16,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -68,6 +69,7 @@ export class LoginCommand { protected policyApiService: PolicyApiServiceAbstraction, protected orgService: OrganizationService, protected logoutCallback: () => Promise, + protected kdfConfigService: KdfConfigService, ) {} async run(email: string, password: string, options: OptionValues) { @@ -229,7 +231,7 @@ export class LoginCommand { } } if (response.requiresTwoFactor) { - const twoFactorProviders = this.twoFactorService.getSupportedProviders(null); + const twoFactorProviders = await this.twoFactorService.getSupportedProviders(null); if (twoFactorProviders.length === 0) { return Response.badRequest("No providers available for this client."); } @@ -270,7 +272,7 @@ export class LoginCommand { if ( twoFactorToken == null && - response.twoFactorProviders.size > 1 && + Object.keys(response.twoFactorProviders).length > 1 && selectedProvider.type === TwoFactorProviderType.Email ) { const emailReq = new TwoFactorEmailRequest(); @@ -563,14 +565,12 @@ export class LoginCommand { message: "Master Password Hint (optional):", }); const masterPasswordHint = hint.input; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); // Create new key and hash new password const newMasterKey = await this.cryptoService.makeMasterKey( masterPassword, this.email.trim().toLowerCase(), - kdf, kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey(masterPassword, newMasterKey); diff --git a/apps/cli/src/auth/commands/unlock.command.ts b/apps/cli/src/auth/commands/unlock.command.ts index d52468139a..6b97b59c88 100644 --- a/apps/cli/src/auth/commands/unlock.command.ts +++ b/apps/cli/src/auth/commands/unlock.command.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; @@ -34,6 +35,7 @@ export class UnlockCommand { private syncService: SyncService, private organizationApiService: OrganizationApiServiceAbstraction, private logout: () => Promise, + private kdfConfigService: KdfConfigService, ) {} async run(password: string, cmdOptions: Record) { @@ -48,9 +50,8 @@ export class UnlockCommand { await this.setNewSessionKey(); const email = await this.stateService.getEmail(); - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); - const masterKey = await this.cryptoService.makeMasterKey(password, email, kdf, kdfConfig); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); + const masterKey = await this.cryptoService.makeMasterKey(password, email, kdfConfig); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const storedMasterKeyHash = await firstValueFrom( this.masterPasswordService.masterKeyHash$(userId), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 7fbefc10e3..a038f3aa90 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -3,6 +3,7 @@ import * as path from "path"; import { program } from "commander"; import * as jsdom from "jsdom"; +import { firstValueFrom } from "rxjs"; import { InternalUserDecryptionOptionsServiceAbstraction, @@ -28,14 +29,16 @@ import { ProviderApiService } from "@bitwarden/common/admin-console/services/pro import { ProviderService } from "@bitwarden/common/admin-console/services/provider.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; @@ -60,10 +63,10 @@ import { } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { KeySuffixOptions, LogLevelType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { AppIdService } from "@bitwarden/common/platform/services/app-id.service"; -import { BroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; import { ConfigApiService } from "@bitwarden/common/platform/services/config/config-api.service"; import { DefaultConfigService } from "@bitwarden/common/platform/services/config/default-config.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; @@ -75,9 +78,9 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ActiveUserStateProvider, DerivedStateProvider, @@ -155,7 +158,7 @@ global.DOMParser = new jsdom.JSDOM().window.DOMParser; const packageJson = require("../package.json"); export class Main { - messagingService: NoopMessagingService; + messagingService: MessageSender; storageService: LowdbStorageService; secureStorageService: NodeEnvSecureStorageService; memoryStorageService: MemoryStorageService; @@ -212,14 +215,13 @@ export class Main { organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; - broadcasterService: BroadcasterService; folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; syncNotifierService: SyncNotifierService; sendApiService: SendApiService; devicesApiService: DevicesApiServiceAbstraction; - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction; + deviceTrustService: DeviceTrustServiceAbstraction; authRequestService: AuthRequestService; configApiService: ConfigApiServiceAbstraction; configService: ConfigService; @@ -235,6 +237,8 @@ export class Main { biometricStateService: BiometricStateService; billingAccountProfileStateService: BillingAccountProfileStateService; providerApiService: ProviderApiServiceAbstraction; + userAutoUnlockKeyService: UserAutoUnlockKeyService; + kdfConfigService: KdfConfigServiceAbstraction; constructor() { let p = null; @@ -298,7 +302,7 @@ export class Main { stateEventRegistrarService, ); - this.messagingService = new NoopMessagingService(); + this.messagingService = MessageSender.EMPTY; this.accountService = new AccountServiceImplementation( this.messagingService, @@ -311,9 +315,7 @@ export class Main { this.singleUserStateProvider, ); - this.derivedStateProvider = new DefaultDerivedStateProvider( - this.memoryStorageForStateProviders, - ); + this.derivedStateProvider = new DefaultDerivedStateProvider(); this.stateProvider = new DefaultStateProvider( this.activeUserStateProvider, @@ -343,6 +345,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Cli, ); this.stateService = new StateService( @@ -359,6 +362,8 @@ export class Main { this.masterPasswordService = new MasterPasswordService(this.stateProvider); + this.kdfConfigService = new KdfConfigService(this.stateProvider); + this.cryptoService = new CryptoService( this.masterPasswordService, this.keyGenerationService, @@ -369,6 +374,7 @@ export class Main { this.stateService, this.accountService, this.stateProvider, + this.kdfConfigService, ); this.appIdService = new AppIdService(this.globalStateProvider); @@ -422,8 +428,6 @@ export class Main { this.searchService = new SearchService(this.logService, this.i18nService, this.stateProvider); - this.broadcasterService = new BroadcasterService(); - this.collectionService = new CollectionService( this.cryptoService, this.i18nService, @@ -453,7 +457,11 @@ export class Main { this.stateProvider, ); - this.twoFactorService = new TwoFactorService(this.i18nService, this.platformUtilsService); + this.twoFactorService = new TwoFactorService( + this.i18nService, + this.platformUtilsService, + this.globalStateProvider, + ); this.passwordStrengthService = new PasswordStrengthService(); @@ -466,7 +474,7 @@ export class Main { this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); - this.deviceTrustCryptoService = new DeviceTrustCryptoService( + this.deviceTrustService = new DeviceTrustService( this.keyGenerationService, this.cryptoFunctionService, this.cryptoService, @@ -478,6 +486,7 @@ export class Main { this.stateProvider, this.secureStorageService, this.userDecryptionOptionsService, + this.logService, ); this.authRequestService = new AuthRequestService( @@ -511,11 +520,12 @@ export class Main { this.encryptService, this.passwordStrengthService, this.policyService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, + this.kdfConfigService, ); this.authService = new AuthService( @@ -578,6 +588,7 @@ export class Main { this.cryptoService, this.vaultTimeoutSettingsService, this.logService, + this.kdfConfigService, ); this.userVerificationService = new UserVerificationService( @@ -592,6 +603,7 @@ export class Main { this.logService, this.vaultTimeoutSettingsService, this.platformUtilsService, + this.kdfConfigService, ); this.vaultTimeoutService = new VaultTimeoutService( @@ -600,7 +612,6 @@ export class Main { this.cipherService, this.folderService, this.collectionService, - this.cryptoService, this.platformUtilsService, this.messagingService, this.searchService, @@ -637,6 +648,7 @@ export class Main { this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, + this.tokenService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); @@ -657,7 +669,7 @@ export class Main { this.cipherService, this.cryptoService, this.cryptoFunctionService, - this.stateService, + this.kdfConfigService, ); this.organizationExportService = new OrganizationVaultExportService( @@ -665,8 +677,8 @@ export class Main { this.apiService, this.cryptoService, this.cryptoFunctionService, - this.stateService, this.collectionService, + this.kdfConfigService, ); this.exportService = new VaultExportService( @@ -697,6 +709,8 @@ export class Main { ); this.providerApiService = new ProviderApiService(this.apiService); + + this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService); } async run() { @@ -717,7 +731,7 @@ export class Main { this.authService.logOut(() => { /* Do nothing */ }); - const userId = await this.stateService.getUserId(); + const userId = (await this.stateService.getUserId()) as UserId; await Promise.all([ this.eventUploadService.uploadEvents(userId as UserId), this.syncService.setLastSync(new Date(0)), @@ -728,9 +742,10 @@ export class Main { this.passwordGenerationService.clear(), ]); - await this.stateEventRunnerService.handleEvent("logout", userId as UserId); + await this.stateEventRunnerService.handleEvent("logout", userId); await this.stateService.clean(); + await this.accountService.clean(userId); process.env.BW_SESSION = null; } @@ -740,6 +755,11 @@ export class Main { this.containerService.attachToGlobal(global); await this.i18nService.init(); this.twoFactorService.init(); + + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount) { + await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); + } } } diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 3d4f9529ad..e64ff8b551 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -86,8 +86,7 @@ export class EditCommand { cipherView = CipherExport.toView(req, cipherView); const encCipher = await this.cipherService.encrypt(cipherView); try { - await this.cipherService.updateWithServer(encCipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.updateWithServer(encCipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); @@ -111,8 +110,7 @@ export class EditCommand { cipher.collectionIds = req; try { - await this.cipherService.saveCollectionsWithServer(cipher); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/cli/src/commands/serve.command.ts b/apps/cli/src/commands/serve.command.ts index 76447f769c..7a11dc4b4a 100644 --- a/apps/cli/src/commands/serve.command.ts +++ b/apps/cli/src/commands/serve.command.ts @@ -134,6 +134,7 @@ export class ServeCommand { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); this.sendCreateCommand = new SendCreateCommand( diff --git a/apps/cli/src/platform/services/console-log.service.spec.ts b/apps/cli/src/platform/services/console-log.service.spec.ts index 10a0ad8cca..03598b16e6 100644 --- a/apps/cli/src/platform/services/console-log.service.spec.ts +++ b/apps/cli/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "@bitwarden/common/spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any = {}; - describe("CLI Console log service", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; let logService: ConsoleLogService; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -19,24 +24,21 @@ describe("CLI Console log service", () => { it("should redirect all console to error if BW_RESPONSE env is true", () => { process.env.BW_RESPONSE = "true"; - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("this is a debug message", error, obj); }); it("should not redirect console to error if BW_RESPONSE != true", () => { process.env.BW_RESPONSE = "false"; - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toMatchObject({ - log: { 0: "info" }, - warn: { 0: "warning" }, - error: { 0: "error" }, - }); + expect(consoleSpy.log).toHaveBeenCalledWith("debug", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("info", error, obj); + expect(consoleSpy.warn).toHaveBeenCalledWith("warning", error, obj); + expect(consoleSpy.error).toHaveBeenCalledWith("error", error, obj); }); }); diff --git a/apps/cli/src/platform/services/console-log.service.ts b/apps/cli/src/platform/services/console-log.service.ts index a35dae71fc..5bdc0b4015 100644 --- a/apps/cli/src/platform/services/console-log.service.ts +++ b/apps/cli/src/platform/services/console-log.service.ts @@ -6,17 +6,17 @@ export class ConsoleLogService extends BaseConsoleLogService { super(isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } if (process.env.BW_RESPONSE === "true") { // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); return; } - super.write(level, message); + super.write(level, message, ...optionalParams); } } diff --git a/apps/cli/src/program.ts b/apps/cli/src/program.ts index fa71a88f54..5d26b0850e 100644 --- a/apps/cli/src/program.ts +++ b/apps/cli/src/program.ts @@ -156,6 +156,7 @@ export class Program { this.main.policyApiService, this.main.organizationService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(email, password, options); this.processResponse(response, true); @@ -265,6 +266,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, async () => await this.main.logout(), + this.main.kdfConfigService, ); const response = await command.run(password, cmd); this.processResponse(response); @@ -627,6 +629,7 @@ export class Program { this.main.syncService, this.main.organizationApiService, this.main.logout, + this.main.kdfConfigService, ); const response = await command.run(null, null); if (!response.success) { diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index b813227109..78ee04e73c 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -80,8 +80,7 @@ export class CreateCommand { private async createCipher(req: CipherExport) { const cipher = await this.cipherService.encrypt(CipherExport.toView(req)); try { - await this.cipherService.createWithServer(cipher); - const newCipher = await this.cipherService.get(cipher.id); + const newCipher = await this.cipherService.createWithServer(cipher); const decCipher = await newCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(newCipher), ); @@ -142,12 +141,11 @@ export class CreateCommand { } try { - await this.cipherService.saveAttachmentRawWithServer( + const updatedCipher = await this.cipherService.saveAttachmentRawWithServer( cipher, fileName, new Uint8Array(fileBuf).buffer, ); - const updatedCipher = await this.cipherService.get(cipher.id); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher), ); diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index cb408a87d8..1f4f624dc8 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,5 +1,5 @@ { - "dev_flags": {}, + "devFlags": {}, "flags": { "multithreadDecryption": false, "enableCipherKeyEncryption": false diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 4f0d05581c..960d56b036 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -228,7 +228,8 @@ "artifactName": "${productName}-${version}-${arch}.${ext}" }, "snap": { - "summary": "After installation enable required `password-manager-service` by running `sudo snap connect bitwarden:password-manager-service`.", + "summary": "Bitwarden is a secure and free password manager for all of your devices.", + "description": "**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.", "autoStart": true, "base": "core22", "confinement": "strict", diff --git a/apps/desktop/native-messaging-test-runner/package-lock.json b/apps/desktop/native-messaging-test-runner/package-lock.json index e2961eb9ee..747d8ec981 100644 --- a/apps/desktop/native-messaging-test-runner/package-lock.json +++ b/apps/desktop/native-messaging-test-runner/package-lock.json @@ -20,15 +20,17 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "typescript": "4.7.4" } }, "../../../libs/common": { + "name": "@bitwarden/common", "version": "0.0.0", "license": "GPL-3.0" }, "../../../libs/node": { + "name": "@bitwarden/node", "version": "0.0.0", "license": "GPL-3.0", "dependencies": { @@ -105,9 +107,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" diff --git a/apps/desktop/native-messaging-test-runner/package.json b/apps/desktop/native-messaging-test-runner/package.json index c572613119..72b2587a4a 100644 --- a/apps/desktop/native-messaging-test-runner/package.json +++ b/apps/desktop/native-messaging-test-runner/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@tsconfig/node16": "1.0.4", "@types/node": "18.19.29", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "typescript": "4.7.4" }, "_moduleAliases": { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 4bb0ab2d93..90d9841a61 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,7 +1,7 @@ { "name": "@bitwarden/desktop", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.2", + "version": "2024.5.0", "keywords": [ "bitwarden", "password", diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index 5f59530d8c..f3958d7c87 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -14,6 +14,7 @@ import { DeviceType } from "@bitwarden/common/enums"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -27,6 +28,7 @@ import { DialogService } from "@bitwarden/components"; import { SetPinComponent } from "../../auth/components/set-pin.component"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; +import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service"; @Component({ selector: "app-settings", @@ -126,6 +128,8 @@ export class SettingsComponent implements OnInit { private biometricStateService: BiometricStateService, private desktopAutofillSettingsService: DesktopAutofillSettingsService, private authRequestService: AuthRequestServiceAbstraction, + private logService: LogService, + private nativeMessagingManifestService: NativeMessagingManifestService, ) { const isMac = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop; @@ -628,11 +632,20 @@ export class SettingsComponent implements OnInit { } await this.stateService.setEnableBrowserIntegration(this.form.value.enableBrowserIntegration); - this.messagingService.send( - this.form.value.enableBrowserIntegration - ? "enableBrowserIntegration" - : "disableBrowserIntegration", + + const errorResult = await this.nativeMessagingManifestService.generate( + this.form.value.enableBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationErrorTitle" }, + content: { key: "browserIntegrationErrorDesc" }, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "danger", + }); + } if (!this.form.value.enableBrowserIntegration) { this.form.controls.enableBrowserIntegrationFingerprint.setValue(false); @@ -647,15 +660,28 @@ export class SettingsComponent implements OnInit { this.form.value.enableDuckDuckGoBrowserIntegration, ); + // Adding to cover users on a previous version of DDG + await this.stateService.setEnableDuckDuckGoBrowserIntegration( + this.form.value.enableDuckDuckGoBrowserIntegration, + ); + if (!this.form.value.enableBrowserIntegration) { await this.stateService.setDuckDuckGoSharedKey(null); } - this.messagingService.send( - this.form.value.enableDuckDuckGoBrowserIntegration - ? "enableDuckDuckGoBrowserIntegration" - : "disableDuckDuckGoBrowserIntegration", + const errorResult = await this.nativeMessagingManifestService.generateDuckDuckGo( + this.form.value.enableDuckDuckGoBrowserIntegration, ); + if (errorResult !== null) { + this.logService.error("Error in DDG browser integration: " + errorResult); + await this.dialogService.openSimpleDialog({ + title: { key: "browserIntegrationUnsupportedTitle" }, + content: errorResult.message, + acceptButtonText: { key: "ok" }, + cancelButtonText: null, + type: "warning", + }); + } } async saveBrowserIntegrationFingerprint() { diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 4fc19c8433..bb8deb2339 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -9,7 +9,7 @@ import { } from "@bitwarden/angular/auth/guards"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; -import { LoginGuard } from "../auth/guards/login.guard"; +import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { HintComponent } from "../auth/hint.component"; import { LockComponent } from "../auth/lock.component"; import { LoginDecryptionOptionsComponent } from "../auth/login/login-decryption-options/login-decryption-options.component"; @@ -40,7 +40,7 @@ const routes: Routes = [ { path: "login", component: LoginComponent, - canActivate: [LoginGuard], + canActivate: [maxAccountsGuardFn()], }, { path: "login-with-device", diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index b2b44e6b21..056fb3f51e 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -3,15 +3,12 @@ import { NgZone, OnDestroy, OnInit, - SecurityContext, Type, ViewChild, ViewContainerRef, } from "@angular/core"; -import { DomSanitizer } from "@angular/platform-browser"; import { Router } from "@angular/router"; -import { IndividualConfig, ToastrService } from "ngx-toastr"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { filter, firstValueFrom, map, Subject, takeUntil, timeout } from "rxjs"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -21,9 +18,9 @@ import { NotificationsService } from "@bitwarden/common/abstractions/notificatio import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; -import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; @@ -49,7 +46,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -110,11 +107,11 @@ export class AppComponent implements OnInit, OnDestroy { loading = false; - private lastActivity: number = null; + private lastActivity: Date = null; private modal: ModalRef = null; private idleTimer: number = null; private isIdle = false; - private activeUserId: string = null; + private activeUserId: UserId = null; private destroy$ = new Subject(); @@ -129,9 +126,8 @@ export class AppComponent implements OnInit, OnDestroy { private cipherService: CipherService, private authService: AuthService, private router: Router, - private toastrService: ToastrService, + private toastService: ToastService, private i18nService: I18nService, - private sanitizer: DomSanitizer, private ngZone: NgZone, private vaultTimeoutService: VaultTimeoutService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, @@ -154,12 +150,12 @@ export class AppComponent implements OnInit, OnDestroy { private biometricStateService: BiometricStateService, private stateEventRunnerService: StateEventRunnerService, private providerService: ProviderService, - private organizationService: InternalOrganizationServiceAbstraction, + private accountService: AccountService, ) {} ngOnInit() { - this.stateService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((userId) => { - this.activeUserId = userId; + this.accountService.activeAccount$.pipe(takeUntil(this.destroy$)).subscribe((account) => { + this.activeUserId = account?.id; }); this.ngZone.runOutsideAngular(() => { @@ -222,8 +218,10 @@ export class AppComponent implements OnInit, OnDestroy { await this.vaultTimeoutService.lock(message.userId); break; case "lockAllVaults": { - const currentUser = await this.stateService.getUserId(); - const accounts = await firstValueFrom(this.stateService.accounts$); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a.id)), + ); + const accounts = await firstValueFrom(this.accountService.accounts$); await this.vaultTimeoutService.lock(currentUser); for (const account of Object.keys(accounts)) { if (account === currentUser) { @@ -294,7 +292,7 @@ export class AppComponent implements OnInit, OnDestroy { ); break; case "showToast": - this.showToast(message); + this.toastService._showToast(message); break; case "copiedToClipboard": if (!message.clearing) { @@ -404,7 +402,8 @@ export class AppComponent implements OnInit, OnDestroy { break; case "switchAccount": { if (message.userId != null) { - await this.stateService.setActiveUser(message.userId); + await this.stateService.clearDecryptedData(message.userId); + await this.accountService.switchAccount(message.userId); } const locked = (await this.authService.getAuthStatus(message.userId)) === @@ -526,7 +525,7 @@ export class AppComponent implements OnInit, OnDestroy { private async updateAppMenu() { let updateRequest: MenuUpdateRequest; - const stateAccounts = await firstValueFrom(this.stateService.accounts$); + const stateAccounts = await firstValueFrom(this.accountService.accounts$); if (stateAccounts == null || Object.keys(stateAccounts).length < 1) { updateRequest = { accounts: null, @@ -535,50 +534,76 @@ export class AppComponent implements OnInit, OnDestroy { } else { const accounts: { [userId: string]: MenuAccount } = {}; for (const i in stateAccounts) { + const userId = i as UserId; if ( i != null && - stateAccounts[i]?.profile?.userId != null && - !this.isAccountCleanUpInProgress(stateAccounts[i].profile.userId) // skip accounts that are being cleaned up + userId != null && + !this.isAccountCleanUpInProgress(userId) // skip accounts that are being cleaned up ) { - const userId = stateAccounts[i].profile.userId; const availableTimeoutActions = await firstValueFrom( this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId), ); + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); accounts[userId] = { - isAuthenticated: await this.stateService.getIsAuthenticated({ - userId: userId, - }), - isLocked: - (await this.authService.getAuthStatus(userId)) === AuthenticationStatus.Locked, + isAuthenticated: authStatus >= AuthenticationStatus.Locked, + isLocked: authStatus === AuthenticationStatus.Locked, isLockable: availableTimeoutActions.includes(VaultTimeoutAction.Lock), - email: stateAccounts[i].profile.email, - userId: stateAccounts[i].profile.userId, + email: stateAccounts[userId].email, + userId: userId, hasMasterPassword: await this.userVerificationService.hasMasterPassword(userId), }; } } updateRequest = { accounts: accounts, - activeUserId: await this.stateService.getUserId(), + activeUserId: await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ), }; } this.messagingService.send("updateAppMenu", { updateRequest: updateRequest }); } - private async logOut(expired: boolean, userId?: string) { - const userBeingLoggedOut = await this.stateService.getUserId({ userId: userId }); + // Even though the userId parameter is no longer optional doesn't mean a message couldn't be + // passing null-ish values to us. + private async logOut(expired: boolean, userId: UserId) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const userBeingLoggedOut = userId ?? activeUserId; // Mark account as being cleaned up so that the updateAppMenu logic (executed on syncCompleted) // doesn't attempt to update a user that is being logged out as we will manually // call updateAppMenu when the logout is complete. this.startAccountCleanUp(userBeingLoggedOut); - let preLogoutActiveUserId; + const nextUpAccount = + activeUserId === userBeingLoggedOut + ? await firstValueFrom(this.accountService.nextUpAccount$) // We'll need to switch accounts + : null; + try { + // HACK: We shouldn't wait for authentication status to change here but instead subscribe to the + // authentication status to do various actions. + const logoutPromise = firstValueFrom( + this.authService.authStatusFor$(userBeingLoggedOut).pipe( + filter((authenticationStatus) => authenticationStatus === AuthenticationStatus.LoggedOut), + timeout({ + first: 5_000, + with: () => { + throw new Error( + "The logout process did not complete in a reasonable amount of time.", + ); + }, + }), + ), + ); + // Provide the userId of the user to upload events for - await this.eventUploadService.uploadEvents(userBeingLoggedOut as UserId); + await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); @@ -586,29 +611,37 @@ export class AppComponent implements OnInit, OnDestroy { await this.collectionService.clear(userBeingLoggedOut); await this.passwordGenerationService.clear(userBeingLoggedOut); await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut); - await this.biometricStateService.logout(userBeingLoggedOut as UserId); + await this.biometricStateService.logout(userBeingLoggedOut); - await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut as UserId); + await this.stateEventRunnerService.handleEvent("logout", userBeingLoggedOut); - preLogoutActiveUserId = this.activeUserId; await this.stateService.clean({ userId: userBeingLoggedOut }); + await this.accountService.clean(userBeingLoggedOut); + + // HACK: Wait for the user logging outs authentication status to transition to LoggedOut + await logoutPromise; } finally { this.finishAccountCleanUp(userBeingLoggedOut); } - if (this.activeUserId == null) { - // 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(["login"]); - } else if (preLogoutActiveUserId !== this.activeUserId) { - this.messagingService.send("switchAccount"); + // We only need to change the display at all if the account being looked at is the one + // being logged out. If it was a background account, no need to do anything. + if (userBeingLoggedOut === activeUserId) { + if (nextUpAccount != null) { + this.messagingService.send("switchAccount", { userId: nextUpAccount.id }); + } else { + // We don't have another user to switch to, bring them to the login page so they + // can sign into a user. + await this.accountService.switchAccount(null); + void this.router.navigate(["login"]); + } } await this.updateAppMenu(); // This must come last otherwise the logout will prematurely trigger // a process reload before all the state service user data can be cleaned up - if (userBeingLoggedOut === preLogoutActiveUserId) { + if (userBeingLoggedOut === activeUserId) { this.authService.logOut(async () => { if (expired) { this.platformUtilsService.showToast( @@ -626,13 +659,13 @@ export class AppComponent implements OnInit, OnDestroy { return; } - const now = new Date().getTime(); - if (this.lastActivity != null && now - this.lastActivity < 250) { + const now = new Date(); + if (this.lastActivity != null && now.getTime() - this.lastActivity.getTime() < 250) { return; } this.lastActivity = now; - await this.stateService.setLastActive(now, { userId: this.activeUserId }); + await this.accountService.setAccountActivity(this.activeUserId, now); // Idle states if (this.isIdle) { @@ -674,34 +707,6 @@ export class AppComponent implements OnInit, OnDestroy { }); } - private showToast(msg: any) { - let message = ""; - - const options: Partial = {}; - - if (typeof msg.text === "string") { - message = msg.text; - } else if (msg.text.length === 1) { - message = msg.text[0]; - } else { - msg.text.forEach( - (t: string) => - (message += "

" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "

"), - ); - options.enableHtml = true; - } - if (msg.options != null) { - if (msg.options.trustedHtml === true) { - options.enableHtml = true; - } - if (msg.options.timeout != null && msg.options.timeout > 0) { - options.timeOut = msg.options.timeout; - } - } - - this.toastrService.show(message, msg.title, options, "toast-" + msg.type); - } - private routeToVault(action: string, cipherType: CipherType) { if (!this.router.url.includes("vault")) { // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -717,7 +722,7 @@ export class AppComponent implements OnInit, OnDestroy { } private async checkForSystemTimeout(timeout: number): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); for (const userId in accounts) { if (userId == null) { continue; @@ -727,7 +732,7 @@ export class AppComponent implements OnInit, OnDestroy { // 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 options[1] === "logOut" - ? this.logOut(false, userId) + ? this.logOut(false, userId as UserId) : await this.vaultTimeoutService.lock(userId); } } diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index eedafbcfe0..b5741a1a1b 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -1,110 +1,112 @@ - - - - - -
- - - - - {{ "accountSwitcherLimitReached" | i18n }} - + + + -
- + + {{ "switchAccount" | i18n }} + + + + + + + + diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 4e39ab0029..92cfebfd60 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -1,19 +1,17 @@ import { animate, state, style, transition, trigger } from "@angular/animations"; import { ConnectedPosition } from "@angular/cdk/overlay"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { Router } from "@angular/router"; -import { concatMap, firstValueFrom, Subject, takeUntil } from "rxjs"; +import { combineLatest, firstValueFrom, map, Observable, switchMap } from "rxjs"; import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Account } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; type ActiveAccount = { @@ -52,12 +50,18 @@ type InactiveAccount = ActiveAccount & { ]), ], }) -export class AccountSwitcherComponent implements OnInit, OnDestroy { - activeAccount?: ActiveAccount; - inactiveAccounts: { [userId: string]: InactiveAccount } = {}; - +export class AccountSwitcherComponent { + activeAccount$: Observable; + inactiveAccounts$: Observable<{ [userId: string]: InactiveAccount }>; authStatus = AuthenticationStatus; + view$: Observable<{ + activeAccount: ActiveAccount | null; + inactiveAccounts: { [userId: string]: InactiveAccount }; + numberOfAccounts: number; + showSwitcher: boolean; + }>; + isOpen = false; overlayPosition: ConnectedPosition[] = [ { @@ -68,21 +72,9 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { }, ]; - private destroy$ = new Subject(); + showSwitcher$: Observable; - get showSwitcher() { - const userIsInAVault = !Utils.isNullOrWhitespace(this.activeAccount?.email); - const userIsAddingAnAdditionalAccount = Object.keys(this.inactiveAccounts).length > 0; - return userIsInAVault || userIsAddingAnAdditionalAccount; - } - - get numberOfAccounts() { - if (this.inactiveAccounts == null) { - this.isOpen = false; - return 0; - } - return Object.keys(this.inactiveAccounts).length; - } + numberOfAccounts$: Observable; constructor( private stateService: StateService, @@ -90,37 +82,70 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { private avatarService: AvatarService, private messagingService: MessagingService, private router: Router, - private tokenService: TokenService, private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, - ) {} + private accountService: AccountService, + ) { + this.activeAccount$ = this.accountService.activeAccount$.pipe( + switchMap(async (active) => { + if (active == null) { + return null; + } - async ngOnInit(): Promise { - this.stateService.accounts$ - .pipe( - concatMap(async (accounts: { [userId: string]: Account }) => { - this.inactiveAccounts = await this.createInactiveAccounts(accounts); + if (!active.name && !active.email) { + // We need to have this information at minimum to display them. + return null; + } - try { - this.activeAccount = { - id: await this.tokenService.getUserId(), - name: (await this.tokenService.getName()) ?? (await this.tokenService.getEmail()), - email: await this.tokenService.getEmail(), - avatarColor: await firstValueFrom(this.avatarService.avatarColor$), - server: (await this.environmentService.getEnvironment())?.getHostname(), - }; - } catch { - this.activeAccount = undefined; - } - }), - takeUntil(this.destroy$), - ) - .subscribe(); - } + return { + id: active.id, + name: active.name, + email: active.email, + avatarColor: await firstValueFrom(this.avatarService.avatarColor$), + server: (await this.environmentService.getEnvironment())?.getHostname(), + }; + }), + ); + this.inactiveAccounts$ = combineLatest([ + this.activeAccount$, + this.accountService.accounts$, + this.authService.authStatuses$, + ]).pipe( + switchMap(async ([activeAccount, accounts, accountStatuses]) => { + // Filter out logged out accounts and active account + accounts = Object.fromEntries( + Object.entries(accounts).filter( + ([id]: [UserId, AccountInfo]) => + accountStatuses[id] !== AuthenticationStatus.LoggedOut || id === activeAccount?.id, + ), + ); + return this.createInactiveAccounts(accounts); + }), + ); + this.showSwitcher$ = combineLatest([this.activeAccount$, this.inactiveAccounts$]).pipe( + map(([activeAccount, inactiveAccounts]) => { + const hasActiveUser = activeAccount != null; + const userIsAddingAnAdditionalAccount = Object.keys(inactiveAccounts).length > 0; + return hasActiveUser || userIsAddingAnAdditionalAccount; + }), + ); + this.numberOfAccounts$ = this.inactiveAccounts$.pipe( + map((accounts) => Object.keys(accounts).length), + ); - ngOnDestroy(): void { - this.destroy$.next(); - this.destroy$.complete(); + this.view$ = combineLatest([ + this.activeAccount$, + this.inactiveAccounts$, + this.numberOfAccounts$, + this.showSwitcher$, + ]).pipe( + map(([activeAccount, inactiveAccounts, numberOfAccounts, showSwitcher]) => ({ + activeAccount, + inactiveAccounts, + numberOfAccounts, + showSwitcher, + })), + ); } toggle() { @@ -144,11 +169,13 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { await this.loginEmailService.saveEmailSettings(); await this.router.navigate(["/login"]); - await this.stateService.setActiveUser(null); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + await this.stateService.clearDecryptedData(activeAccount?.id as UserId); + await this.accountService.switchAccount(null); } private async createInactiveAccounts(baseAccounts: { - [userId: string]: Account; + [userId: string]: AccountInfo; }): Promise<{ [userId: string]: InactiveAccount }> { const inactiveAccounts: { [userId: string]: InactiveAccount } = {}; @@ -159,8 +186,8 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy { inactiveAccounts[userId] = { id: userId, - name: baseAccounts[userId].profile.name, - email: baseAccounts[userId].profile.email, + name: baseAccounts[userId].name, + email: baseAccounts[userId].email, authenticationStatus: await this.authService.getAuthStatus(userId), avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)), server: (await this.environmentService.getEnvironment(userId))?.getHostname(), diff --git a/apps/desktop/src/app/layout/search/search.component.ts b/apps/desktop/src/app/layout/search/search.component.ts index 9a7226218a..06c67d8af2 100644 --- a/apps/desktop/src/app/layout/search/search.component.ts +++ b/apps/desktop/src/app/layout/search/search.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { UntypedFormControl } from "@angular/forms"; import { Subscription } from "rxjs"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { SearchBarService, SearchBarState } from "./search-bar.service"; @@ -18,7 +18,7 @@ export class SearchComponent implements OnInit, OnDestroy { constructor( private searchBarService: SearchBarService, - private stateService: StateService, + private accountService: AccountService, ) { // eslint-disable-next-line rxjs-angular/prefer-takeuntil this.searchBarService.state$.subscribe((state) => { @@ -33,7 +33,7 @@ export class SearchComponent implements OnInit, OnDestroy { ngOnInit() { // eslint-disable-next-line rxjs-angular/prefer-takeuntil - this.activeAccountSubscription = this.stateService.activeAccount$.subscribe((value) => { + this.activeAccountSubscription = this.accountService.activeAccount$.subscribe((_) => { this.searchBarService.setSearchText(""); this.searchText.patchValue(""); }); diff --git a/apps/desktop/src/app/services/init.service.ts b/apps/desktop/src/app/services/init.service.ts index d1a83d468c..0452e9be83 100644 --- a/apps/desktop/src/app/services/init.service.ts +++ b/apps/desktop/src/app/services/init.service.ts @@ -1,10 +1,12 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -12,8 +14,10 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; @@ -35,6 +39,8 @@ export class InitService { private nativeMessagingService: NativeMessagingService, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userAutoUnlockKeyService: UserAutoUnlockKeyService, + private accountService: AccountService, @Inject(DOCUMENT) private document: Document, ) {} @@ -42,6 +48,19 @@ export class InitService { return async () => { this.nativeMessagingService.init(); await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process + + const accounts = await firstValueFrom(this.accountService.accounts$); + const setUserKeyInMemoryPromises = []; + for (const userId of Object.keys(accounts) as UserId[]) { + // For each acct, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access + // the user key from mem. + setUserKeyInMemoryPromises.push( + this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(userId), + ); + } + await Promise.all(setUserKeyInMemoryPromises); + // 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.syncService.fullSync(true); diff --git a/apps/desktop/src/app/services/native-messaging-manifest.service.ts b/apps/desktop/src/app/services/native-messaging-manifest.service.ts new file mode 100644 index 0000000000..6cc58a581b --- /dev/null +++ b/apps/desktop/src/app/services/native-messaging-manifest.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from "@angular/core"; + +@Injectable() +export class NativeMessagingManifestService { + constructor() {} + + async generate(create: boolean): Promise { + return ipc.platform.nativeMessaging.manifests.generate(create); + } + async generateDuckDuckGo(create: boolean): Promise { + return ipc.platform.nativeMessaging.manifests.generateDuckDuckGo(create); + } +} diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 8e412d4977..2acf6dde5a 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -1,9 +1,9 @@ import { APP_INITIALIZER, NgModule } from "@angular/core"; +import { Subject, merge } from "rxjs"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { SECURE_STORAGE, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -14,16 +14,19 @@ import { SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, STATE_FACTORY, + INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { ClientType } from "@bitwarden/common/enums"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -42,6 +45,9 @@ import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/ import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -53,7 +59,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService } from "@bitwarden/components"; -import { LoginGuard } from "../../auth/guards/login.guard"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { Account } from "../../models/account"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; @@ -63,11 +68,12 @@ import { ELECTRON_SUPPORTS_SECURE_STORAGE, ElectronPlatformUtilsService, } from "../../platform/services/electron-platform-utils.service"; -import { ElectronRendererMessagingService } from "../../platform/services/electron-renderer-messaging.service"; +import { ElectronRendererMessageSender } from "../../platform/services/electron-renderer-message.sender"; import { ElectronRendererSecureStorageService } from "../../platform/services/electron-renderer-secure-storage.service"; import { ElectronRendererStorageService } from "../../platform/services/electron-renderer-storage.service"; import { ElectronStateService } from "../../platform/services/electron-state.service"; import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; +import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; @@ -76,6 +82,7 @@ import { SearchBarService } from "../layout/search/search-bar.service"; import { DesktopFileDownloadService } from "./desktop-file-download.service"; import { InitService } from "./init.service"; +import { NativeMessagingManifestService } from "./native-messaging-manifest.service"; import { RendererCryptoFunctionService } from "./renderer-crypto-function.service"; const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); @@ -94,7 +101,6 @@ const safeProviders: SafeProvider[] = [ safeProvider(InitService), safeProvider(NativeMessagingService), safeProvider(SearchBarService), - safeProvider(LoginGuard), safeProvider(DialogService), safeProvider({ provide: APP_INITIALIZER as SafeInjectionToken<() => void>, @@ -137,9 +143,24 @@ const safeProviders: SafeProvider[] = [ deps: [SYSTEM_LANGUAGE, LOCALES_DIRECTORY, GlobalStateProvider], }), safeProvider({ - provide: MessagingServiceAbstraction, - useClass: ElectronRendererMessagingService, - deps: [BroadcasterServiceAbstraction], + provide: MessageSender, + useFactory: (subject: Subject>) => + MessageSender.combine( + new ElectronRendererMessageSender(), // Communication with main process + new SubjectMessageSender(subject), // Communication with ourself + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => + new MessageListener( + merge( + subject.asObservable(), // For messages from the same context + fromIpcMessaging(), // For messages from the main process + ), + ), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], }), safeProvider({ provide: AbstractStorageService, @@ -169,6 +190,7 @@ const safeProviders: SafeProvider[] = [ AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, BiometricStateService, + AccountServiceAbstraction, ], }), safeProvider({ @@ -184,7 +206,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenService, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ @@ -200,7 +221,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: EncryptedMessageHandlerService, deps: [ - StateServiceAbstraction, + AccountServiceAbstraction, AuthServiceAbstraction, CipherServiceAbstraction, PolicyServiceAbstraction, @@ -239,6 +260,7 @@ const safeProviders: SafeProvider[] = [ AccountServiceAbstraction, StateProvider, BiometricStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -249,6 +271,15 @@ const safeProviders: SafeProvider[] = [ provide: DesktopAutofillSettingsService, deps: [StateProvider], }), + safeProvider({ + provide: NativeMessagingManifestService, + useClass: NativeMessagingManifestService, + deps: [], + }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Desktop, + }), ]; @NgModule({ diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index 51b5bf93a2..d908de8ef7 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -4,6 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -59,6 +60,10 @@ describe("GeneratorComponent", () => { provide: CipherService, useValue: mock(), }, + { + provide: AccountService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/app/tools/send/add-edit.component.ts b/apps/desktop/src/app/tools/send/add-edit.component.ts index 7bdd5efbba..804a390438 100644 --- a/apps/desktop/src/app/tools/send/add-edit.component.ts +++ b/apps/desktop/src/app/tools/send/add-edit.component.ts @@ -4,6 +4,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -34,6 +35,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService: DialogService, formBuilder: FormBuilder, billingAccountProfileStateService: BillingAccountProfileStateService, + accountService: AccountService, ) { super( i18nService, @@ -49,6 +51,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); } diff --git a/apps/desktop/src/auth/guards/login.guard.ts b/apps/desktop/src/auth/guards/login.guard.ts deleted file mode 100644 index f6c67d5af9..0000000000 --- a/apps/desktop/src/auth/guards/login.guard.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from "@angular/core"; -import { CanActivate } from "@angular/router"; -import { firstValueFrom } from "rxjs"; - -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; - -const maxAllowedAccounts = 5; - -@Injectable() -export class LoginGuard implements CanActivate { - protected homepage = "vault"; - constructor( - private stateService: StateService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - ) {} - - async canActivate() { - const accounts = await firstValueFrom(this.stateService.accounts$); - if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { - this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached")); - return false; - } - - return true; - } -} diff --git a/apps/desktop/src/auth/guards/max-accounts.guard.ts b/apps/desktop/src/auth/guards/max-accounts.guard.ts new file mode 100644 index 0000000000..65c4ac99d0 --- /dev/null +++ b/apps/desktop/src/auth/guards/max-accounts.guard.ts @@ -0,0 +1,38 @@ +import { inject } from "@angular/core"; +import { CanActivateFn } from "@angular/router"; +import { Observable, map } from "rxjs"; + +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ToastService } from "@bitwarden/components"; + +const maxAllowedAccounts = 5; + +function maxAccountsGuard(): Observable { + const authService = inject(AuthService); + const toastService = inject(ToastService); + const i18nService = inject(I18nService); + + return authService.authStatuses$.pipe( + map((statuses) => + Object.values(statuses).filter((status) => status != AuthenticationStatus.LoggedOut), + ), + map((accounts) => { + if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) { + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accountLimitReached"), + }); + return false; + } + + return true; + }), + ); +} + +export function maxAccountsGuardFn(): CanActivateFn { + return () => maxAccountsGuard(); +} diff --git a/apps/desktop/src/auth/lock.component.spec.ts b/apps/desktop/src/auth/lock.component.spec.ts index c125eba022..2137b707f6 100644 --- a/apps/desktop/src/auth/lock.component.spec.ts +++ b/apps/desktop/src/auth/lock.component.spec.ts @@ -13,7 +13,9 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; @@ -49,7 +51,7 @@ describe("LockComponent", () => { let component: LockComponent; let fixture: ComponentFixture; let stateServiceMock: MockProxy; - const biometricStateService = mock(); + let biometricStateService: MockProxy; let messagingServiceMock: MockProxy; let broadcasterServiceMock: MockProxy; let platformUtilsServiceMock: MockProxy; @@ -61,7 +63,6 @@ describe("LockComponent", () => { beforeEach(async () => { stateServiceMock = mock(); - stateServiceMock.activeAccount$ = of(null); messagingServiceMock = mock(); broadcasterServiceMock = mock(); @@ -72,6 +73,7 @@ describe("LockComponent", () => { mockMasterPasswordService = new FakeMasterPasswordService(); + biometricStateService = mock(); biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false); biometricStateService.promptAutomatically$ = of(false); biometricStateService.promptCancelled$ = of(false); @@ -145,8 +147,8 @@ describe("LockComponent", () => { useValue: mock(), }, { - provide: DeviceTrustCryptoServiceAbstraction, - useValue: mock(), + provide: DeviceTrustServiceAbstraction, + useValue: mock(), }, { provide: UserVerificationService, @@ -164,6 +166,14 @@ describe("LockComponent", () => { provide: AccountService, useValue: accountService, }, + { + provide: AuthService, + useValue: mock(), + }, + { + provide: KdfConfigService, + useValue: mock(), + }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); diff --git a/apps/desktop/src/auth/lock.component.ts b/apps/desktop/src/auth/lock.component.ts index 16b58c5bbe..d95df419e1 100644 --- a/apps/desktop/src/auth/lock.component.ts +++ b/apps/desktop/src/auth/lock.component.ts @@ -10,7 +10,9 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { DeviceType } from "@bitwarden/common/enums"; @@ -58,11 +60,13 @@ export class LockComponent extends BaseLockComponent { passwordStrengthService: PasswordStrengthServiceAbstraction, logService: LogService, dialogService: DialogService, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, userVerificationService: UserVerificationService, pinCryptoService: PinCryptoServiceAbstraction, biometricStateService: BiometricStateService, accountService: AccountService, + authService: AuthService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -82,11 +86,13 @@ export class LockComponent extends BaseLockComponent { policyService, passwordStrengthService, dialogService, - deviceTrustCryptoService, + deviceTrustService, userVerificationService, pinCryptoService, biometricStateService, accountService, + authService, + kdfConfigService, ); } diff --git a/apps/desktop/src/auth/login/login-via-auth-request.component.ts b/apps/desktop/src/auth/login/login-via-auth-request.component.ts index 0a339030ba..2d0f560205 100644 --- a/apps/desktop/src/auth/login/login-via-auth-request.component.ts +++ b/apps/desktop/src/auth/login/login-via-auth-request.component.ts @@ -13,7 +13,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -55,7 +55,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { syncService: SyncService, stateService: StateService, loginEmailService: LoginEmailServiceAbstraction, - deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + deviceTrustService: DeviceTrustServiceAbstraction, authRequestService: AuthRequestServiceAbstraction, loginStrategyService: LoginStrategyServiceAbstraction, accountService: AccountService, @@ -77,7 +77,7 @@ export class LoginViaAuthRequestComponent extends BaseLoginWithDeviceComponent { validationService, stateService, loginEmailService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, loginStrategyService, accountService, diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 93dfe0abd8..feea5edd86 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -9,6 +9,7 @@ import { OrganizationUserService } from "@bitwarden/common/admin-console/abstrac import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -52,6 +53,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -73,6 +75,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On userDecryptionOptionsService, ssoLoginService, dialogService, + kdfConfigService, ); } diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index ee32c045c9..97067b788a 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Verander Hoofwagwoord" }, - "changeMasterPasswordConfirmation": { - "message": "U kan u hoofwagwoord op die bitwarden.com-webkluis verander. Wil u die webwerf nou besoek?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Vingerafdrukfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Blaaierintegrasie word nie ondersteun nie" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 104a9f7780..74c3d63e33 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغيير كلمة المرور الرئيسية" }, - "changeMasterPasswordConfirmation": { - "message": "يمكنك تغيير كلمة المرور الرئيسية من خزنة الويب في bitwarden.com. هل تريد زيارة الموقع الآن؟" + "continueToWebApp": { + "message": "متابعة إلى تطبيق الويب؟" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "يمكنك تغيير كلمة المرور الرئيسية الخاصة بك على تطبيق ويب Bitwarden." }, "fingerprintPhrase": { "message": "عبارة بصمة الإصبع", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "تكامل المتصفح غير مدعوم" }, + "browserIntegrationErrorTitle": { + "message": "خطأ في تمكين تكامل المتصفح" + }, + "browserIntegrationErrorDesc": { + "message": "حدث خطأ أثناء تمكين دمج المتصفح." + }, "browserIntegrationMasOnlyDesc": { "message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي." }, @@ -1645,10 +1654,10 @@ "message": "أضف طبقة أمان إضافية عن طريق طلب تأكيد عبارة بصمة الإصبع عند إنشاء رابط بين سطح المكتب الخاص بك والمتصفح. هذا يتطلب إجراء المستخدم والتحقق في كل مرة يتم فيها إنشاء اتصال." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "استخدام تسارع العتاد" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "بشكل افتراضي هذا الإعداد مفعل. أوقف التشغيل فقط إذا واجهت مشاكل في الرسوم البيانية. إعادة التشغيل مطلوبة." }, "approve": { "message": "الموافقة" @@ -2688,19 +2697,28 @@ "message": "تنسيقات مشتركة", "description": "Label indicating the most common import formats" }, + "success": { + "message": "نجاح" + }, "troubleshooting": { - "message": "Troubleshooting" + "message": "استكشاف الأخطاء وإصلاحها" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "تعطيل تسارع العتاد وإعادة التشغيل" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "تمكين تسارع العتاد وإعادة التشغيل" }, "removePasskey": { - "message": "Remove passkey" + "message": "إزالة مفتاح المرور" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "تمت إزالة كلمة المرور" + }, + "errorAssigningTargetCollection": { + "message": "خطأ في تعيين مجموعة الأهداف." + }, + "errorAssigningTargetFolder": { + "message": "خطأ في تعيين مجلد الهدف." } } diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index f404c7f95a..1ecd18eee7 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -717,7 +717,7 @@ "message": "Bildiriş server URL-si" }, "iconsUrl": { - "message": "Nişan server URL-si" + "message": "İkon server URL-si" }, "environmentSaved": { "message": "Mühit URL-ləri saxlanıldı." @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolu dəyişdir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolunuzu bitwarden.com veb anbarında dəyişdirə bilərsiniz. İndi saytı ziyarət etmək istəyirsiniz?" + "continueToWebApp": { + "message": "Veb tətbiqlə davam edilsin?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolunuzu Bitwarden veb tətbiqində dəyişdirə bilərsiniz." }, "fingerprintPhrase": { "message": "Barmaq izi ifadəsi", @@ -920,52 +923,52 @@ "description": "Clipboard is the operating system thing where you copy/paste data to on your device." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən bir təsvir göstər." }, "enableMinToTray": { - "message": "Bildiriş nişanına kiçildin" + "message": "Bildiriş sahəsi ikonuna kiçilt" }, "enableMinToTrayDesc": { - "message": "Pəncərə kiçildiləndə, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə kiçildiləndə, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableMinToMenuBar": { - "message": "Menyu sətrinə kiçilt" + "message": "Menyu çubuğuna kiçilt" }, "enableMinToMenuBarDesc": { - "message": "Pəncərəni kiçildəndə, menyu sətrində bir nişan göstər." + "message": "Pəncərəni kiçildəndə, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableCloseToTray": { - "message": "Bildiriş nişanına bağla" + "message": "Bildiriş ikonu üçün bağla" }, "enableCloseToTrayDesc": { - "message": "Pəncərə bağlananda, bildiriş sahəsində bir nişan göstər." + "message": "Pəncərə bağladılanda, bunun əvəzinə bildiriş sahəsində bir ikon göstər." }, "enableCloseToMenuBar": { - "message": "Menyu sətrini bağla" + "message": "Menyu çubuğunda bağla" }, "enableCloseToMenuBarDesc": { - "message": "Pəncərəni bağlananda, menyu sətrində bir nişan göstər." + "message": "Pəncərəni bağladılanda, bunun əvəzinə menyu çubuğunda bir ikon göstər." }, "enableTray": { - "message": "Bildiriş sahəsi nişanını fəallaşdır" + "message": "Bildiriş sahəsi ikonunu göstər" }, "enableTrayDesc": { - "message": "Bildiriş sahəsində həmişə bir nişan göstər." + "message": "Bildiriş sahəsində həmişə bir ikon göstər." }, "startToTray": { - "message": "Bildiriş sahəsi nişanı kimi başlat" + "message": "Bildiriş sahəsi ikonu kimi başlat" }, "startToTrayDesc": { - "message": "Tətbiq ilk başladılanda, yalnız bildiriş sahəsi nişanı görünsün." + "message": "Tətbiq ilk başladılanda, sistem bildiriş sahəsində yalnız ikon olaraq görünsün." }, "startToMenuBar": { - "message": "Menyu sətrini başlat" + "message": "Menyu çubuğunda başlat" }, "startToMenuBarDesc": { - "message": "Tətbiq ilk başladılanda, menyu sətri sadəcə nişan kimi görünsün." + "message": "Tətbiq ilk başladılanda, menyu çubuğunda yalnız ikon olaraq görünsün." }, "openAtLogin": { "message": "Giriş ediləndə avtomatik başlat" @@ -977,7 +980,7 @@ "message": "\"Dock\"da həmişə göstər" }, "alwaysShowDockDesc": { - "message": "Menyu sətrinə kiçildiləndə belə Bitwarden nişanını \"Dock\"da göstər." + "message": "Menyu çubuğuna kiçildiləndə belə Bitwarden ikonunu Yuvada göstər." }, "confirmTrayTitle": { "message": "Bildiriş sahəsi nişanını ləğv et" @@ -1450,16 +1453,16 @@ "message": "Hesabınız bağlandı və bütün əlaqəli datalar silindi." }, "preferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "enableMenuBar": { - "message": "Menyu sətri nişanını fəallaşdır" + "message": "Menyu çubuğu ikonunu göstər" }, "enableMenuBarDesc": { - "message": "Menyu sətrində həmişə bir nişan göstər." + "message": "Menyu çubuğunda həmişə bir ikon göstər." }, "hideToMenuBar": { - "message": "Menyu sətrini gizlət" + "message": "Menyu çubuğunda gizlət" }, "selectOneCollection": { "message": "Ən azı bir kolleksiya seçməlisiniz." @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauzer inteqrasiyası dəstəklənmir" }, + "browserIntegrationErrorTitle": { + "message": "Brauzer inteqrasiyasını fəallaşdırma xətası" + }, + "browserIntegrationErrorDesc": { + "message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi." + }, "browserIntegrationMasOnlyDesc": { "message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir." }, @@ -2021,7 +2030,7 @@ "message": "Eyni vaxtda 5-dən çox hesaba giriş edilə bilməz." }, "accountPreferences": { - "message": "Tercihlər" + "message": "Tərcihlər" }, "appPreferences": { "message": "Tətbiq ayarları (bütün hesablar)" @@ -2688,6 +2697,9 @@ "message": "Ortaq formatlar", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Uğurlu" + }, "troubleshooting": { "message": "Problemlərin aradan qaldırılması" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Parol silindi" + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." } } diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index 2bc33f2b28..e0133e5a74 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змяніць асноўны пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можаце змяніць свой асноўны пароль у вэб-сховішчы на bitwarden.com. Перайсці на вэб-сайт зараз?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Фраза адбітка пальца", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтэграцыя з браўзерам не падтрымліваецца" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 9f6d5bdd36..0728178b7b 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -15,7 +15,7 @@ "message": "Видове" }, "typeLogin": { - "message": "Запис" + "message": "Вход" }, "typeCard": { "message": "Карта" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промяна на главната парола" }, - "changeMasterPasswordConfirmation": { - "message": "Главната парола на трезор може да се промени чрез сайта bitwarden.com. Искате ли да го посетите?" + "continueToWebApp": { + "message": "Продължаване към уеб приложението?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Може да промените главната си парола в уеб приложението на Битуорден." }, "fingerprintPhrase": { "message": "Уникална фраза", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграцията с браузър не се поддържа" }, + "browserIntegrationErrorTitle": { + "message": "Грешка при включването на интеграцията с браузъра" + }, + "browserIntegrationErrorDesc": { + "message": "Възникна грешка при включването на интеграцията с браузъра." + }, "browserIntegrationMasOnlyDesc": { "message": "За жалост в момента интеграцията с браузър не се поддържа във версията за магазина на Mac." }, @@ -2688,6 +2697,9 @@ "message": "Често използвани формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успех" + }, "troubleshooting": { "message": "Отстраняване на проблеми" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Секретният ключ е премахнат" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." } } diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 22893aadab..626734ebff 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "মূল পাসওয়ার্ড পরিবর্তন" }, - "changeMasterPasswordConfirmation": { - "message": "আপনি bitwarden.com ওয়েব ভল্ট থেকে মূল পাসওয়ার্ডটি পরিবর্তন করতে পারেন। আপনি কি এখনই ওয়েবসাইটটি দেখতে চান?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ফিঙ্গারপ্রিন্ট ফ্রেজ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index 6adb5bbce3..9d5685cca9 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promijenite glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Možete da promjenite svoju glavnu lozinku na bitwarden.com web trezoru. Da li želite da posjetite web stranicu sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija sa preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index c76111b53c..d7100af764 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Canvia la contrasenya mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Podeu canviar la contrasenya mestra a la caixa forta web de bitwarden.com. Voleu visitar el lloc web ara?" + "continueToWebApp": { + "message": "Continua cap a l'aplicació web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Podeu canviar la vostra contrasenya mestra a l'aplicació web de Bitwarden." }, "fingerprintPhrase": { "message": "Frase d'empremta digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integració en el navegador no és compatible" }, + "browserIntegrationErrorTitle": { + "message": "S'ha produït un error en habilitar la integració del navegador" + }, + "browserIntegrationErrorDesc": { + "message": "S'ha produït un error en activar la integració del navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Malauradament, la integració del navegador només és compatible amb la versió de Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Formats comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Èxit" + }, "troubleshooting": { "message": "Resolució de problemes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clau de pas suprimida" + }, + "errorAssigningTargetCollection": { + "message": "S'ha produït un error en assignar la col·lecció de destinació." + }, + "errorAssigningTargetFolder": { + "message": "S'ha produït un error en assignar la carpeta de destinació." } } diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index e7ba56e81c..e68fe8fffc 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Změnit hlavní heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Hlavní heslo si můžete změnit na webové stránce bitwarden.com. Chcete tuto stránku nyní otevřít?" + "continueToWebApp": { + "message": "Pokračovat do webové aplikace?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavní heslo můžete změnit ve webové aplikaci Bitwardenu." }, "fingerprintPhrase": { "message": "Fráze otisku prstu", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrace prohlížeče není podporována" }, + "browserIntegrationErrorTitle": { + "message": "Chyba při povolování integrace prohlížeče" + }, + "browserIntegrationErrorDesc": { + "message": "Vyskytla se chyba při povolování integrace prohlížeče." + }, "browserIntegrationMasOnlyDesc": { "message": "Integrace prohlížeče je podporována jen ve verzi pro Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Společné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspěch" + }, "troubleshooting": { "message": "Řešení problémů" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Přístupový klíč byl odebrán" + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." } } diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index e87b805d0b..62f2e608bb 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 1f994cf8eb..0e578a6f66 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Skift hovedadgangskode" }, - "changeMasterPasswordConfirmation": { - "message": "Man kan ændre sin hovedadgangskode via bitwarden.com web-boksen. Besøg webstedet nu?" + "continueToWebApp": { + "message": "Fortsæt til web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hovedadgangskoden kan ændres via Bitwarden web-appen." }, "fingerprintPhrase": { "message": "Fingeraftrykssætning", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegration understøttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Fejl ved aktivering af webbrowserintegration" + }, + "browserIntegrationErrorDesc": { + "message": "En fejl opstod under aktivering af webbrowserintegration." + }, "browserIntegrationMasOnlyDesc": { "message": "Desværre understøttes browserintegration indtil videre kun i Mac App Store-versionen." }, @@ -2688,6 +2697,9 @@ "message": "Almindelige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Gennemført" + }, "troubleshooting": { "message": "Fejlfinding" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Adgangsnøgle fjernet" + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." } } diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 428cfd6a27..d04c2795f3 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Master-Passwort ändern" }, - "changeMasterPasswordConfirmation": { - "message": "Du kannst dein Master-Passwort im bitwarden.com-Web-Tresor ändern. Möchtest du die Seite jetzt öffnen?" + "continueToWebApp": { + "message": "Weiter zur Web-App?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Du kannst dein Master-Passwort in der Bitwarden Web-App ändern." }, "fingerprintPhrase": { "message": "Fingerabdruck-Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser-Integration wird nicht unterstützt" }, + "browserIntegrationErrorTitle": { + "message": "Fehler beim Aktivieren der Browser-Integration" + }, + "browserIntegrationErrorDesc": { + "message": "Beim Aktivieren der Browser-Integration ist ein Fehler aufgetreten." + }, "browserIntegrationMasOnlyDesc": { "message": "Leider wird die Browser-Integration derzeit nur in der Mac App Store Version unterstützt." }, @@ -2688,6 +2697,9 @@ "message": "Gängigste Formate", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Erfolg" + }, "troubleshooting": { "message": "Problembehandlung" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey entfernt" + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." } } diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 63b1f21c2e..87360c33ce 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Αλλαγή Κύριου Κωδικού" }, - "changeMasterPasswordConfirmation": { - "message": "Μπορείτε να αλλάξετε τον κύριο κωδικό στο bitwarden.com. Θέλετε να επισκεφθείτε την ιστοσελίδα τώρα;" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Φράση Δακτυλικών Αποτυπωμάτων", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Η ενσωμάτωση του περιηγητή δεν υποστηρίζεται" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Δυστυχώς η ενσωμάτωση του προγράμματος περιήγησης υποστηρίζεται μόνο στην έκδοση Mac App Store για τώρα." }, @@ -2688,6 +2697,9 @@ "message": "Κοινές μορφές", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Αντιμετώπιση Προβλημάτων" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 00eace54e2..ff9cbc97cc 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -1632,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2691,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 53958bca57..5572d2fd35 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1546,11 +1549,11 @@ "message": "Set master password" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -1690,7 +1699,7 @@ "message": "An organisation policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "allSends": { "message": "All Sends", @@ -1886,7 +1895,7 @@ "message": "Your master password was recently changed by an administrator in your organisation. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tryAgain": { "message": "Try again" @@ -1944,7 +1953,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1961,7 +1970,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2051,7 +2060,7 @@ "message": "Exporting individual vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2337,7 +2346,7 @@ "message": "Region" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2523,7 +2532,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index f6011c301f..79f2de8f63 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1546,11 +1549,11 @@ "message": "Set master password" }, "orgPermissionsUpdatedMustSetPassword": { - "message": "Your organization permissions were updated, requiring you to set a master password.", + "message": "Your organisation permissions were updated, requiring you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "orgRequiresYouToSetPassword": { - "message": "Your organization requires you to set a master password.", + "message": "Your organisation requires you to set a master password.", "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -1690,7 +1699,7 @@ "message": "An organization policy is affecting your ownership options." }, "personalOwnershipPolicyInEffectImports": { - "message": "An organization policy has blocked importing items into your individual vault." + "message": "An organisation policy has blocked importing items into your individual vault." }, "allSends": { "message": "All Sends", @@ -1886,7 +1895,7 @@ "message": "Your master password was recently changed by an administrator in your organization. In order to access the vault, you must update it now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "updateWeakMasterPasswordWarning": { - "message": "Your master password does not meet one or more of your organization policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." + "message": "Your master password does not meet one or more of your organisation policies. In order to access the vault, you must update your master password now. Proceeding will log you out of your current session, requiring you to log back in. Active sessions on other devices may continue to remain active for up to one hour." }, "tryAgain": { "message": "Try again" @@ -1944,7 +1953,7 @@ } }, "vaultTimeoutPolicyWithActionInEffect": { - "message": "Your organization policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", + "message": "Your organisation policies are affecting your vault timeout. Maximum allowed vault timeout is $HOURS$ hour(s) and $MINUTES$ minute(s). Your vault timeout action is set to $ACTION$.", "placeholders": { "hours": { "content": "$1", @@ -1961,7 +1970,7 @@ } }, "vaultTimeoutActionPolicyInEffect": { - "message": "Your organization policies have set your vault timeout action to $ACTION$.", + "message": "Your organisation policies have set your vault timeout action to $ACTION$.", "placeholders": { "action": { "content": "$1", @@ -2051,7 +2060,7 @@ "message": "Exporting individual vault" }, "exportingIndividualVaultDescription": { - "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.", + "message": "Only the individual vault items associated with $EMAIL$ will be exported. Organisation vault items will not be included. Only vault item information will be exported and will not include associated attachments.", "placeholders": { "email": { "content": "$1", @@ -2337,7 +2346,7 @@ "message": "Region" }, "ssoIdentifierRequired": { - "message": "Organization SSO identifier is required." + "message": "Organisation SSO identifier is required." }, "eu": { "message": "EU", @@ -2523,7 +2532,7 @@ "message": "Total" }, "importWarning": { - "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organization. Do you want to proceed?", + "message": "You are importing data to $ORGANIZATION$. Your data may be shared with members of this organisation. Do you want to proceed?", "placeholders": { "organization": { "content": "$1", @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index 772eb70985..427f08f805 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index e3dcd0dc4c..f7df93bdd7 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambiar contraseña maestra" }, - "changeMasterPasswordConfirmation": { - "message": "Puedes cambiar tu contraseña maestra en la caja fuerte web de bitwarden.com. ¿Quieres visitar ahora el sitio web?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frase de huella digital", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "La integración con el navegador no está soportada" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Por desgracia la integración del navegador sólo está soportada por ahora en la versión de la Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 2b54df2a91..02cd737baa 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Muuda ülemparooli" }, - "changeMasterPasswordConfirmation": { - "message": "Saad oma ülemparooli muuta bitwarden.com veebihoidlas. Soovid seda kohe teha?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Sõrmejälje fraas", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Brauseri integratsioon ei ole toetatud" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Paraku on brauseri integratsioon hetkel toetatud ainult Mac App Store'i versioonis." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index d66d5265e1..2067b2dcc2 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Aldatu pasahitz nagusia" }, - "changeMasterPasswordConfirmation": { - "message": "Zure pasahitz nagusia alda dezakezu bitwarden.com webgunean. Orain joan nahi duzu webgunera?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hatz-marka digitalaren esaldia", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Ez da nabigatzailearen integrazioa onartzen" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Zoritxarrez, Mac App Storeren bertsioan soilik onartzen da oraingoz nabigatzailearen integrazioa." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index c62bb99b2d..ef34f8222a 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "تغییر کلمه عبور اصلی" }, - "changeMasterPasswordConfirmation": { - "message": "شما می‌توانید کلمه عبور اصلی خود را در bitwarden.com تغییر دهید. آیا می‌خواهید از سایت بازدید کنید؟" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "عبارت اثر انگشت", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ادغام مرورگر پشتیبانی نمی‌شود" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "متأسفانه در حال حاضر ادغام مرورگر فقط در نسخه Mac App Store پشتیبانی می‌شود." }, @@ -2688,6 +2697,9 @@ "message": "فرمت‌های رایج", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f74136aedc..517b437d03 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Vaihda pääsalasana" }, - "changeMasterPasswordConfirmation": { - "message": "Voit vaihtaa pääsalasanasi bitwarden.com-verkkoholvissa. Haluatko käydä sivustolla nyt?" + "continueToWebApp": { + "message": "Avataanko verkkosovellus?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Voit vaihtaa pääsalasanasi Bitwardenin verkkosovelluksessa." }, "fingerprintPhrase": { "message": "Tunnistelauseke", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Selainintegraatiota ei tueta" }, + "browserIntegrationErrorTitle": { + "message": "Virhe otettaessa selainintegrointia käyttöön" + }, + "browserIntegrationErrorDesc": { + "message": "Otettaessa selainintegraatiota käyttöön tapahtui virhe." + }, "browserIntegrationMasOnlyDesc": { "message": "Valitettavasti selainintegraatiota tuetaan toistaiseksi vain Mac App Store -versiossa." }, @@ -2688,6 +2697,9 @@ "message": "Yleiset muodot", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Onnistui" + }, "troubleshooting": { "message": "Vianetsintä" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Suojausavain poistettiin" + }, + "errorAssigningTargetCollection": { + "message": "Virhe määritettäessä kohdekokoelmaa." + }, + "errorAssigningTargetFolder": { + "message": "Virhe määritettäessä kohdekansiota." } } diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 170559fc64..d28a4b568c 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Palitan ang master password" }, - "changeMasterPasswordConfirmation": { - "message": "Maaari mong palitan ang iyong master password sa bitwarden.com web vault. Gusto mo bang bisitahin ang website ngayon?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Hulmabig ng Hilik ng Dako", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Hindi suportado ang pagsasama ng browser" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sa kasamaang palad ang pagsasama ng browser ay suportado lamang sa bersyon ng Mac App Store para sa ngayon." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 20353d2d86..8298544d2e 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Changer le mot de passe principal" }, - "changeMasterPasswordConfirmation": { - "message": "Vous pouvez changer votre mot de passe principal depuis le coffre web de bitwarden.com. Voulez-vous visiter le site web maintenant ?" + "continueToWebApp": { + "message": "Poursuivre vers l'application web ?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Vous pouvez modifier votre mot de passe principal sur l'application web de Bitwarden." }, "fingerprintPhrase": { "message": "Phrase d'empreinte", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Intégration dans le navigateur non supportée" }, + "browserIntegrationErrorTitle": { + "message": "Erreur lors de l'intégration avec le navigateur" + }, + "browserIntegrationErrorDesc": { + "message": "Une erreur s'est produite lors de l'action de l'intégration du navigateur." + }, "browserIntegrationMasOnlyDesc": { "message": "Malheureusement l'intégration avec le navigateur est uniquement supportée dans la version Mac App Store pour le moment." }, @@ -2688,6 +2697,9 @@ "message": "Formats communs", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Succès" + }, "troubleshooting": { "message": "Résolution de problèmes" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Clé d'identification (passkey) retirée" + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." } } diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index cc5a0f011d..3b155ffdf3 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "החלף סיסמה ראשית" }, - "changeMasterPasswordConfirmation": { - "message": "באפשרותך לשנות את הסיסמה הראשית שלך דרך הכספת באתר bitwarden.com. האם ברצונך לפתוח את האתר כעת?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "סיסמת טביעת אצבע", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "שילוב הדפדפן אינו נתמך" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "למצער, אינטגרצייה עם הדפדפן בשלב זה נתמכת רק על ידי גרסת חנות האפליקציות של מקינטוש." }, @@ -2688,6 +2697,9 @@ "message": "תסדירים נפוצים", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 57ef1d32ef..af28c66681 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index 1e501cee78..01983d5891 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjeni glavnu lozinku" }, - "changeMasterPasswordConfirmation": { - "message": "Svoju glavnu lozinku možeš promijeniti na web trezoru. Želiš li sada posjetiti bitwarden.com?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Jedinstvena fraza", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integracija preglednika nije podržana" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nažalost, za sada je integracija s preglednikom podržana samo u Mac App Store verziji aplikacije." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 0b443c9a6b..838b3fc7c8 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mesterjelszó módosítása" }, - "changeMasterPasswordConfirmation": { - "message": "A mesterjelszó megváltoztatható a bitwarden.com webes széfben. Szeretnénk felkeresni a webhelyet mos?" + "continueToWebApp": { + "message": "Tovább a webes alkalmazáshoz?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "A mesterjelszó a Bitwarden webalkalmazásban módosítható." }, "fingerprintPhrase": { "message": "Azonosítókifejezés", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "A böngésző integráció nem támogatott." }, + "browserIntegrationErrorTitle": { + "message": "Böngésző integráció engedélyezése" + }, + "browserIntegrationErrorDesc": { + "message": "Hiba történt a böngésző integrációjának engedélyezése közben." + }, "browserIntegrationMasOnlyDesc": { "message": "Sajnos a böngésző integrációt egyelőre csak a Mac App Store verzió támogatja." }, @@ -2688,6 +2697,9 @@ "message": "Általános formátumok", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sikeres" + }, "troubleshooting": { "message": "Hibaelhárítás" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." } } diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index 008b6b369a..2173224f54 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -404,7 +404,7 @@ "message": "Panjang" }, "passwordMinLength": { - "message": "Minimum password length" + "message": "Panjang kata sandi minimum" }, "uppercase": { "message": "Huruf Kapital (A-Z)" @@ -545,7 +545,7 @@ "message": "Diperlukan pengetikan ulang kata sandi utama." }, "masterPasswordMinlength": { - "message": "Master password must be at least $VALUE$ characters long.", + "message": "Kata sandi utama minimal harus $VALUE$ karakter.", "description": "The Master Password must be at least a specific number of characters long.", "placeholders": { "value": { @@ -561,7 +561,7 @@ "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Anda berhasil masuk" }, "youMayCloseThisWindow": { "message": "You may close this window" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ubah Kata Sandi Utama" }, - "changeMasterPasswordConfirmation": { - "message": "Anda dapat mengubah kata sandi utama Anda di brankas web bitwarden.com. Anda ingin mengunjungi situs web sekarang?" + "continueToWebApp": { + "message": "Lanjutkan ke aplikasi web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Anda bisa mengganti kata sandi utama Anda di aplikasi web Bitwarden." }, "fingerprintPhrase": { "message": "Frase Fingerprint", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrasi browser tidak didukung" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Sayangnya integrasi browser hanya didukung di versi Mac App Store untuk saat ini." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index a3a6f771fe..93882cf698 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Cambia password principale" }, - "changeMasterPasswordConfirmation": { - "message": "Puoi cambiare la tua password principale sulla cassaforte online di bitwarden.com. Vuoi visitare ora il sito?" + "continueToWebApp": { + "message": "Passa al sito web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Puoi modificare la tua password principale sul sito web di Bitwarden." }, "fingerprintPhrase": { "message": "Frase impronta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "L'integrazione del browser non è supportata" }, + "browserIntegrationErrorTitle": { + "message": "Errore durante l'attivazione dell'integrazione del browser" + }, + "browserIntegrationErrorDesc": { + "message": "Si è verificato un errore durante l'attivazione dell'integrazione del browser." + }, "browserIntegrationMasOnlyDesc": { "message": "Purtroppo l'integrazione del browser è supportata solo nella versione nell'App Store per ora." }, @@ -2688,6 +2697,9 @@ "message": "Formati comuni", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Successo" + }, "troubleshooting": { "message": "Risoluzione problemi" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey rimossa" + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." } } diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index f0a95d4e35..ab6c0be95f 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "マスターパスワードの変更" }, - "changeMasterPasswordConfirmation": { - "message": "マスターパスワードは bitwarden.com ウェブ保管庫で変更できます。ウェブサイトを開きますか?" + "continueToWebApp": { + "message": "ウェブアプリに進みますか?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Bitwarden ウェブアプリでマスターパスワードを変更できます。" }, "fingerprintPhrase": { "message": "パスフレーズ", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "ブラウザー統合はサポートされていません" }, + "browserIntegrationErrorTitle": { + "message": "ブラウザー連携を有効にする際にエラーが発生しました" + }, + "browserIntegrationErrorDesc": { + "message": "ブラウザー統合の有効化中にエラーが発生しました。" + }, "browserIntegrationMasOnlyDesc": { "message": "残念ながら、ブラウザ統合は、Mac App Storeのバージョンでのみサポートされています。" }, @@ -2688,6 +2697,9 @@ "message": "一般的な形式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "トラブルシューティング" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "パスキーを削除しました" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" } } diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 281d64cbc2..eb0cbcf6be 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "ಮಾಸ್ಟರ್ ಪಾಸ್ವರ್ಡ್ ಬದಲಾಯಿಸಿ" }, - "changeMasterPasswordConfirmation": { - "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ನೀವು bitwarden.com ವೆಬ್ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಬದಲಾಯಿಸಬಹುದು. ನೀವು ಈಗ ವೆಬ್‌ಸೈಟ್‌ಗೆ ಭೇಟಿ ನೀಡಲು ಬಯಸುವಿರಾ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ಫಿಂಗರ್ಪ್ರಿಂಟ್ ಫ್ರೇಸ್", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "ದುರದೃಷ್ಟವಶಾತ್ ಬ್ರೌಸರ್ ಏಕೀಕರಣವನ್ನು ಇದೀಗ ಮ್ಯಾಕ್ ಆಪ್ ಸ್ಟೋರ್ ಆವೃತ್ತಿಯಲ್ಲಿ ಮಾತ್ರ ಬೆಂಬಲಿಸಲಾಗುತ್ತದೆ." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index a09d53b1dd..8e50ade96c 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "마스터 비밀번호 변경" }, - "changeMasterPasswordConfirmation": { - "message": "bitwarden.com 웹 보관함에서 마스터 비밀번호를 바꿀 수 있습니다. 지금 웹 사이트를 방문하시겠습니까?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "지문 구절", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "브라우저와 연결이 지원되지 않음" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "브라우저와 연결은 현재 Mac App Store 버전에서만 지원됩니다." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index dbc2e13d1c..e9de697005 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Keisti pagrindinį slaptažodį" }, - "changeMasterPasswordConfirmation": { - "message": "Pagrindinį slaptažodį galite pakeisti bitwarden.com žiniatinklio saugykloje. Ar norite dabar apsilankyti svetainėje?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Piršto antspaudo frazė", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Naršyklės integravimas nepalaikomas" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Deja, bet naršyklės integravimas kol kas palaikomas tik Mac App Store versijoje." }, @@ -2688,6 +2697,9 @@ "message": "Dažni formatai", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 0a3501dded..e2e068362e 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Mainīt galveno paroli" }, - "changeMasterPasswordConfirmation": { - "message": "Galveno paroli ir iespējams mainīt bitwarden.com tīmekļa glabātavā. Vai tagad apmeklēt tīmekļvietni?" + "continueToWebApp": { + "message": "Pāriet uz tīmekļa lietotni?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Savu galveno paroli var mainīt Bitwarden tīmekļa lietotnē." }, "fingerprintPhrase": { "message": "Atpazīšanas vārdkopa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Sasaistīšana ar pārlūku nav atbalstīta" }, + "browserIntegrationErrorTitle": { + "message": "Kļūda pārlūga saistīšanas iespējošanā" + }, + "browserIntegrationErrorDesc": { + "message": "Atgadījās kļūda pārlūka saistīšanas iespējošanas laikā." + }, "browserIntegrationMasOnlyDesc": { "message": "Diemžēl sasaistīšāna ar pārlūku pagaidām ir nodrošināta tikai Mac App Store laidienā." }, @@ -2688,6 +2697,9 @@ "message": "Izplatīti veidoli", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Izdevās" + }, "troubleshooting": { "message": "Sarežģījumu novēršana" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Piekļuves atslēga noņemta" + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." } } diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index d5e3bddf8e..1f49961b46 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Promjena glavne lozinke" }, - "changeMasterPasswordConfirmation": { - "message": "Možete promijeniti svoju glavnu lozinku u trezoru na internet strani bitwarden.com. Da li želite da posjetite internet lokaciju sada?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fraza računa", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index 6b1137d232..96811b9dba 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "പ്രാഥമിക പാസ്‌വേഡ് മാറ്റുക" }, - "changeMasterPasswordConfirmation": { - "message": "തങ്ങൾക്കു Bitwarden വെബ് വാൾട്ടിൽ പ്രാഥമിക പാസ്‌വേഡ് മാറ്റാൻ സാധിക്കും.വെബ്സൈറ്റ് ഇപ്പോൾ സന്ദർശിക്കാൻ ആഗ്രഹിക്കുന്നുവോ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "ഫിംഗർപ്രിന്റ് ഫ്രേസ്‌", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index 2626e93c24..0ee0db69ef 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index 8e8e2e2cbb..7bf132bdac 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovedpassordet" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre superpassordet ditt på bitwarden.net-netthvelvet. Vil du besøke det nettstedet nå?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrykksfrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Nettleserintegrasjon støttes ikke" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Nettleserintegrasjon støttes dessverre bare i Mac App Store-versjonen for øyeblikket." }, @@ -2688,6 +2697,9 @@ "message": "Vanlige formater", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index e7d586023e..13e1466805 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index 9c4ba78036..f56572259b 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Hoofdwachtwoord wijzigen" }, - "changeMasterPasswordConfirmation": { - "message": "Je kunt je hoofdwachtwoord wijzigen in de kluis op bitwarden.com. Wil je de website nu bezoeken?" + "continueToWebApp": { + "message": "Doorgaan naar web-app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Je kunt je hoofdwachtwoord wijzigen in de Bitwarden-webapp." }, "fingerprintPhrase": { "message": "Vingerafdrukzin", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browserintegratie niet ondersteund" }, + "browserIntegrationErrorTitle": { + "message": "Fout bij inschakelen van de browserintegratie" + }, + "browserIntegrationErrorDesc": { + "message": "Er is iets misgegaan bij het tijdens het inschakelen van de browserintegratie." + }, "browserIntegrationMasOnlyDesc": { "message": "Helaas wordt browserintegratie momenteel alleen ondersteund in de Mac App Store-versie." }, @@ -2688,6 +2697,9 @@ "message": "Veelvoorkomende formaten", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Succes" + }, "troubleshooting": { "message": "Probleemoplossing" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey verwijderd" + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." } } diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index ea55378f5b..35e7173d74 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Endre hovudpassord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan endre hovudpassordet ditt i Bitwarden sin nettkvelv. Vil du gå til nettstaden no?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index c6c0c2fb0c..cd83d2ea69 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index a0626a6c90..250c557309 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmień hasło główne" }, - "changeMasterPasswordConfirmation": { - "message": "Hasło główne możesz zmienić na stronie sejfu bitwarden.com. Czy chcesz przejść do tej strony?" + "continueToWebApp": { + "message": "Kontynuować do aplikacji internetowej?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Możesz zmienić swoje hasło główne w aplikacji internetowej Bitwarden." }, "fingerprintPhrase": { "message": "Unikalny identyfikator konta", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Połączenie z przeglądarką nie jest obsługiwane" }, + "browserIntegrationErrorTitle": { + "message": "Błąd podczas włączania integracji z przeglądarką" + }, + "browserIntegrationErrorDesc": { + "message": "Wystąpił błąd podczas włączania integracji z przeglądarką." + }, "browserIntegrationMasOnlyDesc": { "message": "Połączenie z przeglądarką jest obsługiwane tylko z wersją aplikacji ze sklepu Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Popularne formaty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Sukces" + }, "troubleshooting": { "message": "Rozwiązywanie problemów" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey został usunięty" + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." } } diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index c8f8316e6d..12db01d8cd 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -561,10 +561,10 @@ "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." }, "youSuccessfullyLoggedIn": { - "message": "You successfully logged in" + "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { - "message": "You may close this window" + "message": "Você pode fechar esta janela" }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar Senha Mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Você pode alterar a sua senha mestra no cofre web em bitwarden.com. Você deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continuar no aplicativo web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Você pode alterar a sua senha mestra no aplicativo web Bitwarden." }, "fingerprintPhrase": { "message": "Frase biométrica", @@ -1399,7 +1402,7 @@ "message": "Código PIN inválido." }, "tooManyInvalidPinEntryAttemptsLoggingOut": { - "message": "Too many invalid PIN entry attempts. Logging out." + "message": "Muitas tentativas de entrada de PIN inválidas. Desconectando." }, "unlockWithWindowsHello": { "message": "Desbloquear com o Windows Hello" @@ -1554,7 +1557,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Verificação necessária", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportado" }, + "browserIntegrationErrorTitle": { + "message": "Erro ao ativar a integração do navegador" + }, + "browserIntegrationErrorDesc": { + "message": "Ocorreu um erro ao permitir a integração do navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, por ora, a integração do navegador só é suportada na versão da Mac App Store." }, @@ -1645,10 +1654,10 @@ "message": "Ative uma camada adicional de segurança, exigindo validação de frase de impressão digital ao estabelecer uma ligação entre o computador e o navegador. Quando ativado, isto requer intervenção do usuário e verificação cada vez que uma conexão é estabelecida." }, "enableHardwareAcceleration": { - "message": "Use hardware acceleration" + "message": "Utilizar aceleração de hardware" }, "enableHardwareAccelerationDesc": { - "message": "By default this setting is ON. Turn OFF only if you experience graphical issues. Restart is required." + "message": "Por padrão esta configuração está ativada. Desligar apenas se tiver problemas gráficos. Reiniciar é necessário." }, "approve": { "message": "Aprovar" @@ -1889,40 +1898,40 @@ "message": "A sua senha mestra não atende a uma ou mais das políticas da sua organização. Para acessar o cofre, você deve atualizar a sua senha mestra agora. O processo desconectará você da sessão atual, exigindo que você inicie a sessão novamente. Sessões ativas em outros dispositivos podem continuar ativas por até uma hora." }, "tryAgain": { - "message": "Try again" + "message": "Tentar novamente" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Verificação necessária para esta ação. Defina um PIN para continuar." }, "setPin": { - "message": "Set PIN" + "message": "Definir PIN" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Verificiar com biometria" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Aguardando confirmação" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Não foi possível completar a biometria." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Precisa de um método diferente?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Usar a senha mestra" }, "usePin": { - "message": "Use PIN" + "message": "Usar PIN" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Usar biometria" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "Digite o código de verificação que foi enviado para o seu e-mail." }, "resendCode": { - "message": "Resend code" + "message": "Reenviar código" }, "hours": { "message": "Horas" @@ -2532,13 +2541,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo in Browser" + "message": "Iniciar o Duo no navegador" }, "importFormatError": { "message": "Os dados não estão formatados corretamente. Por favor, verifique o seu arquivo de importação e tente novamente." @@ -2621,13 +2630,13 @@ "message": "Nome de usuário ou senha incorretos" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Senha incorreta" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Código incorreto" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "PIN incorreto" }, "multifactorAuthenticationFailed": { "message": "Falha na autenticação de múltiplos fatores" @@ -2688,19 +2697,28 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { - "message": "Troubleshooting" + "message": "Solução de problemas" }, "disableHardwareAccelerationRestart": { - "message": "Disable hardware acceleration and restart" + "message": "Desativar aceleração de hardware e reiniciar" }, "enableHardwareAccelerationRestart": { - "message": "Enable hardware acceleration and restart" + "message": "Ativar aceleração de hardware e reiniciar" }, "removePasskey": { - "message": "Remove passkey" + "message": "Remover senha" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Chave de acesso removida" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir pasta de destino." } } diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 5fbd7636d1..2535f5d860 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -163,7 +163,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -287,7 +287,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -435,10 +435,10 @@ "message": "Fechar" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos", + "message": "Mínimo de caracteres especiais", "description": "Minimum Special Characters" }, "ambiguous": { @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Alterar palavra-passe mestra" }, - "changeMasterPasswordConfirmation": { - "message": "Pode alterar o seu endereço de e-mail no cofre do site bitwarden.com. Deseja visitar o site agora?" + "continueToWebApp": { + "message": "Continuar para a aplicação Web?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden." }, "fingerprintPhrase": { "message": "Frase de impressão digital", @@ -1289,7 +1292,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integração com o navegador não suportada" }, + "browserIntegrationErrorTitle": { + "message": "Erro ao ativar a integração do navegador" + }, + "browserIntegrationErrorDesc": { + "message": "Ocorreu um erro ao ativar a integração do navegador." + }, "browserIntegrationMasOnlyDesc": { "message": "Infelizmente, a integração do navegador só é suportada na versão da Mac App Store por enquanto." }, @@ -2688,6 +2697,9 @@ "message": "Formatos comuns", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Com sucesso" + }, "troubleshooting": { "message": "Resolução de problemas" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Chave de acesso removida" + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." } } diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 7af8f7ec16..978f57eb9b 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Schimbare parolă principală" }, - "changeMasterPasswordConfirmation": { - "message": "Puteți modifica parola principală pe saitul web bitwarden.com. Doriți să vizitați saitul acum?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Frază amprentă", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrarea browserului nu este acceptată" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Din păcate, integrarea browserului este acceptată numai în versiunea Mac App Store pentru moment." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 0e28c2cf90..c9b3b95b39 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Изменить мастер-пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Вы можете изменить свой мастер-пароль на bitwarden.com. Перейти на сайт сейчас?" + "continueToWebApp": { + "message": "Перейти к веб-приложению?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Изменить мастер-пароль можно в веб-приложении Bitwarden." }, "fingerprintPhrase": { "message": "Фраза отпечатка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграция с браузером не поддерживается" }, + "browserIntegrationErrorTitle": { + "message": "Ошибка при включении интеграции с браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Произошла ошибка при включении интеграции с браузером." + }, "browserIntegrationMasOnlyDesc": { "message": "К сожалению, интеграция браузера пока поддерживается только в версии Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Основные форматы", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успешно" + }, "troubleshooting": { "message": "Устранение проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey удален" + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." } } diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 60e8aea93c..3d43997144 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index 240b883254..6499486b9d 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -445,7 +445,7 @@ "message": "Vyhnúť sa zameniteľným znakom" }, "searchCollection": { - "message": "Search collection" + "message": "Vyhľadať zbierku" }, "searchFolder": { "message": "Search folder" @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Zmeniť hlavné heslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje hlavné heslo môžete zmeniť vo webovom trezore bitwarden.com. Chcete teraz navštíviť túto stránku?" + "continueToWebApp": { + "message": "Pokračovať vo webovej aplikácii?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Hlavné heslo si môžete zmeniť vo webovej aplikácii Bitwarden." }, "fingerprintPhrase": { "message": "Fráza odtlačku", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Integrácia v prehliadači nie je podporovaná" }, + "browserIntegrationErrorTitle": { + "message": "Chyba pri povoľovaní integrácie v prehliadači" + }, + "browserIntegrationErrorDesc": { + "message": "Pri povoľovaní integrácie v prehliadači sa vyskytla chyba." + }, "browserIntegrationMasOnlyDesc": { "message": "Bohužiaľ, integrácia v prehliadači je zatiaľ podporovaná iba vo verzii Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Bežné formáty", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Úspech" + }, "troubleshooting": { "message": "Riešenie problémov" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Prístupový kľúč bol odstránený" + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." } } diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 528274cf29..8cb06dcf0c 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Spremeni glavno geslo" }, - "changeMasterPasswordConfirmation": { - "message": "Svoje glavno geslo lahko spremenite v bitwarden.com spletnem trezorju. Želite obiskati spletno stran zdaj?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Identifikacijsko geslo", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index 77b5f7221d..04b7e4cf29 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Промени главну лозинку" }, - "changeMasterPasswordConfirmation": { - "message": "Можете променити главну лозинку у Вашем сефу на bitwarden.com. Да ли желите да посетите веб страницу сада?" + "continueToWebApp": { + "message": "Ићи на веб апликацију?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Можете променити главну лозинку на Bitwarden веб апликацији." }, "fingerprintPhrase": { "message": "Сигурносна Фраза Сефа", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Интеграција са претраживачем није подржана" }, + "browserIntegrationErrorTitle": { + "message": "Грешка при омогућавању интеграције прегледача" + }, + "browserIntegrationErrorDesc": { + "message": "Дошло је до грешке при омогућавању интеграције прегледача." + }, "browserIntegrationMasOnlyDesc": { "message": "Нажалост, интеграција прегледача за сада је подржана само у верзији Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Уобичајени формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Решавање проблема" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Приступачни кључ је уклоњен" + }, + "errorAssigningTargetCollection": { + "message": "Грешка при додељивању циљне колекције." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при додељивању циљне фасцикле." } } diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index c07f7efef3..bd21c0f328 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ändra huvudlösenord" }, - "changeMasterPasswordConfirmation": { - "message": "Du kan ändra ditt huvudlösenord i Bitwardens webbvalv. Vill du besöka webbplatsen nu?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingeravtrycksfras", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Webbläsarintegration stöds inte" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Tyvärr stöds webbläsarintegration för tillfället endast i versionen från Mac App Store." }, @@ -2688,6 +2697,9 @@ "message": "Vanliga format", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Felsökning" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Nyckel borttagen" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index f96260c005..889a2beeee 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Change master password" }, - "changeMasterPasswordConfirmation": { - "message": "You can change your master password on the bitwarden.com web vault. Do you want to visit the website now?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index efbfc86b33..f1cd5351f7 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "เปลี่ยนรหัสผ่านหลัก" }, - "changeMasterPasswordConfirmation": { - "message": "คุณสามารถเปลี่ยนรหัสผ่านหลักของคุณได้ที่เว็บ bitwarden.com คุณต้องการไปที่เว็บไซต์ตอนนี้ไหม?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Browser integration not supported" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Unfortunately browser integration is only supported in the Mac App Store version for now." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 36d62ed2a5..adeb293ccf 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Ana parolayı değiştir" }, - "changeMasterPasswordConfirmation": { - "message": "Ana parolanızı bitwarden.com web kasası üzerinden değiştirebilirsiniz. Siteyi şimdi ziyaret etmek ister misiniz?" + "continueToWebApp": { + "message": "Web uygulamasına devam edilsin mi?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ana parolanızı Bitwarden web uygulamasında değiştirebilirsiniz." }, "fingerprintPhrase": { "message": "Parmak izi ifadesi", @@ -1554,7 +1557,7 @@ "description": "Used as a card title description on the set password page to explain why the user is there" }, "verificationRequired": { - "message": "Verification required", + "message": "Doğrulama gerekli", "description": "Default title for the user verification dialog." }, "currentMasterPass": { @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Tarayıcı entegrasyonu desteklenmiyor" }, + "browserIntegrationErrorTitle": { + "message": "Tarayıcı entegrasyonunu etkinleştirme hatası" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Ne yazık ki tarayıcı entegrasyonu şu anda sadece Mac App Store sürümünde destekleniyor." }, @@ -1645,10 +1654,10 @@ "message": "Masaüstü uygulamanızla tarayıcınız arasında bağlantı kurulurken parmak izi ifadesi doğrulamasını zorunlu kılarak ek bir güvenlik önlemi alabilirsiniz. Bu ayarı açarsanız her bağlantı kurulduğunda tekrar doğrulama yapmanız gerekir." }, "enableHardwareAcceleration": { - "message": "Donanım hızlandırması kullan" + "message": "Donanım hızlandırmayı kullan" }, "enableHardwareAccelerationDesc": { - "message": "Varsayılan olarak bu ayar AÇIK'tır. Yalnızca grafiksel sorunlarla karşılaşırsanız KAPATIN. Yeniden başlatma gerekli." + "message": "Varsayılan olarak bu ayar AÇIKTIR. Yalnızca grafik sorunlarıyla karşılaşırsanız KAPATIN. Yeniden başlatma gerekir." }, "approve": { "message": "Onayla" @@ -1889,40 +1898,40 @@ "message": "Ana parolanız kuruluş ilkelerinizi karşılamıyor. Kasanıza erişmek için ana parolanızı güncellemelisiniz. Devam ettiğinizde oturumunuz kapanacak ve yeniden oturum açmanız gerekecektir. Diğer cihazlardaki aktif oturumlar bir saate kadar aktif kalabilir." }, "tryAgain": { - "message": "Try again" + "message": "Yeniden dene" }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "Set PIN" + "message": "PIN belirle" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Biyometri ile doğrula" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Onay bekleniyor" }, "couldNotCompleteBiometrics": { - "message": "Could not complete biometrics." + "message": "Biyometri işlemi tamamlanamadı." }, "needADifferentMethod": { - "message": "Need a different method?" + "message": "Farklı bir yönteme mi ihtiyacınız var?" }, "useMasterPassword": { - "message": "Use master password" + "message": "Ana parolayı kullan" }, "usePin": { - "message": "Use PIN" + "message": "PIN kullan" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Biyometri kullan" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { - "message": "Resend code" + "message": "Kodu yeniden gönder" }, "hours": { "message": "Saat" @@ -2538,7 +2547,7 @@ "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "launchDuo": { - "message": "Duo'yu Tarayıcıda Başlat" + "message": "Duo'yu tarayıcıda başlat" }, "importFormatError": { "message": "Veriler doğru biçimlendirilmemiş. Lütfen içe aktarma dosyanızı kontrol edin ve tekrar deneyin." @@ -2621,13 +2630,13 @@ "message": "Kullanıcı adı veya parola yanlış" }, "incorrectPassword": { - "message": "Incorrect password" + "message": "Yanlış parola" }, "incorrectCode": { - "message": "Incorrect code" + "message": "Yanlış kod" }, "incorrectPin": { - "message": "Incorrect PIN" + "message": "Yanlış PIN" }, "multifactorAuthenticationFailed": { "message": "Çok faktörlü kimlik doğrulama başarısız oldu" @@ -2685,22 +2694,31 @@ "message": "LastPass hesabınızla ilişkili YubiKey'i bilgisayarınızın USB portuna takıp düğmesine dokunun." }, "commonImportFormats": { - "message": "Common formats", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Başarılı" + }, "troubleshooting": { "message": "Sorun giderme" }, "disableHardwareAccelerationRestart": { - "message": "Donanım hızlandırmayı devre dışı bırakın ve yeniden başlatın" + "message": "Donanım hızlandırmayı kapatıp yeniden başlat" }, "enableHardwareAccelerationRestart": { - "message": "Donanım hızlandırmayı etkinleştirin ve yeniden başlatın" + "message": "Donanım hızlandırmayı etkinleştirip yeniden başlat" }, "removePasskey": { - "message": "Remove passkey" + "message": "Geçiş anahtarını kaldır" }, "passkeyRemoved": { - "message": "Passkey removed" + "message": "Geçiş anahtarı kaldırıldı" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 377fd23b0b..9ee7652093 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Змінити головний пароль" }, - "changeMasterPasswordConfirmation": { - "message": "Ви можете змінити головний пароль в сховищі на bitwarden.com. Хочете перейти на вебсайт зараз?" + "continueToWebApp": { + "message": "Продовжити у вебпрограмі?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "Ви можете змінити головний пароль у вебпрограмі Bitwarden." }, "fingerprintPhrase": { "message": "Фраза відбитка", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Інтеграція з браузером не підтримується" }, + "browserIntegrationErrorTitle": { + "message": "Помилка увімкнення інтеграції з браузером" + }, + "browserIntegrationErrorDesc": { + "message": "Під час увімкнення інтеграції з браузером сталася помилка." + }, "browserIntegrationMasOnlyDesc": { "message": "На жаль, зараз інтеграція з браузером підтримується лише у версії для Mac з App Store." }, @@ -2688,6 +2697,9 @@ "message": "Поширені формати", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Успішно" + }, "troubleshooting": { "message": "Усунення проблем" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Ключ доступу вилучено" + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." } } diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index 138773d40a..0c0e6f6df7 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "Thay đổi mật khẩu chính" }, - "changeMasterPasswordConfirmation": { - "message": "Bạn có thể thay đổi mật khẩu chính trong kho bitwarden nền web. Bạn có muốn truy cập trang web bây giờ?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "Fingerprint Phrase", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "Không hỗ trợ tích hợp trình duyệt" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "Rất tiếc, tính năng tích hợp trình duyệt hiện chỉ được hỗ trợ trong phiên bản App Store trên Mac." }, @@ -2688,6 +2697,9 @@ "message": "Common formats", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "Troubleshooting" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "Passkey removed" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index 8725fa0f21..aad13e06ef 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "修改主密码" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 网页密码库修改您的主密码。现在要访问吗?" + "continueToWebApp": { + "message": "前往网页 App 吗?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "您可以在 Bitwarden 网页应用上更改您的主密码。" }, "fingerprintPhrase": { "message": "指纹短语", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支持浏览器集成" }, + "browserIntegrationErrorTitle": { + "message": "启用浏览器集成时出错" + }, + "browserIntegrationErrorDesc": { + "message": "启用浏览器集成时出错。" + }, "browserIntegrationMasOnlyDesc": { "message": "很遗憾,目前仅 Mac App Store 版本支持浏览器集成。" }, @@ -2688,6 +2697,9 @@ "message": "常规格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "成功" + }, "troubleshooting": { "message": "故障排除" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "通行密钥已移除" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" } } diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index c93f236976..5f768b0a43 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -800,8 +800,11 @@ "changeMasterPass": { "message": "變更主密碼" }, - "changeMasterPasswordConfirmation": { - "message": "您可以在 bitwarden.com 網頁版密碼庫變更主密碼。現在要前往嗎?" + "continueToWebApp": { + "message": "Continue to web app?" + }, + "changeMasterPasswordOnWebConfirmation": { + "message": "You can change your master password on the Bitwarden web app." }, "fingerprintPhrase": { "message": "指紋短語", @@ -1629,6 +1632,12 @@ "browserIntegrationUnsupportedTitle": { "message": "不支援瀏覽器整合" }, + "browserIntegrationErrorTitle": { + "message": "Error enabling browser integration" + }, + "browserIntegrationErrorDesc": { + "message": "An error has occurred while enabling browser integration." + }, "browserIntegrationMasOnlyDesc": { "message": "很遺憾,目前僅 Mac App Store 版本支援瀏覽器整合功能。" }, @@ -2688,6 +2697,9 @@ "message": "常見格式", "description": "Label indicating the most common import formats" }, + "success": { + "message": "Success" + }, "troubleshooting": { "message": "疑難排解" }, @@ -2702,5 +2714,11 @@ }, "passkeyRemoved": { "message": "金鑰已移除" + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." } } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 67f08839c5..d11fceeacc 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -1,16 +1,20 @@ import * as path from "path"; import { app } from "electron"; -import { firstValueFrom } from "rxjs"; +import { Subject, firstValueFrom } from "rxjs"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/abstractions/token.service"; import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service"; import { TokenService } from "@bitwarden/common/auth/services/token.service"; +import { ClientType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { DefaultBiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- For dependency creation +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; @@ -18,7 +22,6 @@ import { KeyGenerationService } from "@bitwarden/common/platform/services/key-ge import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { NoopMessagingService } from "@bitwarden/common/platform/services/noop-messaging.service"; /* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */ import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider"; @@ -59,7 +62,7 @@ export class Main { storageService: ElectronStorageService; memoryStorageService: MemoryStorageService; memoryStorageForStateProviders: MemoryStorageServiceForStateProviders; - messagingService: ElectronMainMessagingService; + messagingService: MessageSender; stateService: StateService; environmentService: DefaultEnvironmentService; mainCryptoFunctionService: MainCryptoFunctionService; @@ -131,7 +134,7 @@ export class Main { this.i18nService = new I18nMainService("en", "./locales/", globalStateProvider); const accountService = new AccountServiceImplementation( - new NoopMessagingService(), + MessageSender.EMPTY, this.logService, globalStateProvider, ); @@ -155,7 +158,7 @@ export class Main { activeUserStateProvider, singleUserStateProvider, globalStateProvider, - new DefaultDerivedStateProvider(this.memoryStorageForStateProviders), + new DefaultDerivedStateProvider(), ); this.environmentService = new DefaultEnvironmentService(stateProvider, accountService); @@ -188,6 +191,7 @@ export class Main { this.storageService, this.logService, new MigrationBuilderService(), + ClientType.Desktop, ); // TODO: this state service will have access to on disk storage, but not in memory storage. @@ -203,7 +207,6 @@ export class Main { this.environmentService, this.tokenService, this.migrationRunner, - false, // Do not use disk caching because this will get out of sync with the renderer service ); this.desktopSettingsService = new DesktopSettingsService(stateProvider); @@ -223,7 +226,13 @@ export class Main { this.updaterMain = new UpdaterMain(this.i18nService, this.windowMain); this.trayMain = new TrayMain(this.windowMain, this.i18nService, this.desktopSettingsService); - this.messagingService = new ElectronMainMessagingService(this.windowMain, (message) => { + const messageSubject = new Subject>(); + this.messagingService = MessageSender.combine( + new SubjectMessageSender(messageSubject), // For local messages + new ElectronMainMessagingService(this.windowMain), + ); + + messageSubject.asObservable().subscribe((message) => { this.messagingMain.onMessage(message); }); @@ -291,12 +300,20 @@ export class Main { this.powerMonitorMain.init(); await this.updaterMain.init(); - if ( - (await this.stateService.getEnableBrowserIntegration()) || - (await firstValueFrom( - this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$, - )) - ) { + const [browserIntegrationEnabled, ddgIntegrationEnabled] = await Promise.all([ + this.stateService.getEnableBrowserIntegration(), + firstValueFrom(this.desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$), + ]); + + if (browserIntegrationEnabled || ddgIntegrationEnabled) { + // Re-register the native messaging host integrations on startup, in case they are not present + if (browserIntegrationEnabled) { + this.nativeMessagingMain.generateManifests().catch(this.logService.error); + } + if (ddgIntegrationEnabled) { + this.nativeMessagingMain.generateDdgManifests().catch(this.logService.error); + } + this.nativeMessagingMain.listen(); } diff --git a/apps/desktop/src/main/menu/menubar.ts b/apps/desktop/src/main/menu/menubar.ts index eb1dacf825..b71774c5af 100644 --- a/apps/desktop/src/main/menu/menubar.ts +++ b/apps/desktop/src/main/menu/menubar.ts @@ -65,9 +65,10 @@ export class Menubar { isLocked = updateRequest.accounts[updateRequest.activeUserId]?.isLocked ?? true; } - const isLockable = !isLocked && updateRequest?.accounts[updateRequest.activeUserId]?.isLockable; + const isLockable = + !isLocked && updateRequest?.accounts?.[updateRequest.activeUserId]?.isLockable; const hasMasterPassword = - updateRequest?.accounts[updateRequest.activeUserId]?.hasMasterPassword ?? false; + updateRequest?.accounts?.[updateRequest.activeUserId]?.hasMasterPassword ?? false; this.items = [ new FileMenu( diff --git a/apps/desktop/src/main/messaging.main.ts b/apps/desktop/src/main/messaging.main.ts index 256d551560..a9f80b7d20 100644 --- a/apps/desktop/src/main/messaging.main.ts +++ b/apps/desktop/src/main/messaging.main.ts @@ -75,22 +75,6 @@ export class MessagingMain { case "getWindowIsFocused": this.windowIsFocused(); break; - case "enableBrowserIntegration": - this.main.nativeMessagingMain.generateManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "enableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.generateDdgManifests(); - this.main.nativeMessagingMain.listen(); - break; - case "disableBrowserIntegration": - this.main.nativeMessagingMain.removeManifests(); - this.main.nativeMessagingMain.stop(); - break; - case "disableDuckDuckGoBrowserIntegration": - this.main.nativeMessagingMain.removeDdgManifests(); - this.main.nativeMessagingMain.stop(); - break; default: break; } diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 05e987e20b..8c8404578b 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -22,7 +22,55 @@ export class NativeMessagingMain { private windowMain: WindowMain, private userPath: string, private exePath: string, - ) {} + ) { + ipcMain.handle( + "nativeMessaging.manifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateManifests(); + } catch (e) { + this.logService.error("Error generating manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeManifests(); + } catch (e) { + this.logService.error("Error removing manifests: " + e); + return e; + } + } + return null; + }, + ); + + ipcMain.handle( + "nativeMessaging.ddgManifests", + async (_event: any, options: { create: boolean }) => { + if (options.create) { + this.listen(); + try { + await this.generateDdgManifests(); + } catch (e) { + this.logService.error("Error generating duckduckgo manifests: " + e); + return e; + } + } else { + this.stop(); + try { + await this.removeDdgManifests(); + } catch (e) { + this.logService.error("Error removing duckduckgo manifests: " + e); + return e; + } + } + return null; + }, + ); + } listen() { ipc.config.id = "bitwarden"; @@ -76,7 +124,7 @@ export class NativeMessagingMain { ipc.server.emit(socket, "message", message); } - generateManifests() { + async generateManifests() { const baseJson = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> browser bridge", @@ -84,6 +132,10 @@ export class NativeMessagingMain { type: "stdio", }; + if (!existsSync(baseJson.path)) { + throw new Error(`Unable to find binary: ${baseJson.path}`); + } + const firefoxJson = { ...baseJson, ...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] }, @@ -92,8 +144,13 @@ export class NativeMessagingMain { ...baseJson, ...{ allowed_origins: [ + // Chrome extension "chrome-extension://nngceckbapebfimnlniiiahkandclblb/", + // Chrome beta extension + "chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/", + // Edge extension "chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/", + // Opera extension "chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/", ], }, @@ -102,27 +159,17 @@ export class NativeMessagingMain { switch (process.platform) { case "win32": { const destination = path.join(this.userPath, "browsers"); - // 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.writeManifest(path.join(destination, "firefox.json"), firefoxJson); - // 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.writeManifest(path.join(destination, "chrome.json"), chromeJson); + await this.writeManifest(path.join(destination, "firefox.json"), firefoxJson); + await this.writeManifest(path.join(destination, "chrome.json"), chromeJson); - // 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.createWindowsRegistry( - "HKLM\\SOFTWARE\\Mozilla\\Firefox", - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "firefox.json"), - ); - // 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.createWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome", - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - path.join(destination, "chrome.json"), - ); + const nmhs = this.getWindowsNMHS(); + for (const [key, value] of Object.entries(nmhs)) { + let manifestPath = path.join(destination, "chrome.json"); + if (key === "Firefox") { + manifestPath = path.join(destination, "firefox.json"); + } + await this.createWindowsRegistry(value, manifestPath); + } break; } case "darwin": { @@ -136,38 +183,30 @@ export class NativeMessagingMain { manifest = firefoxJson; } - this.writeManifest(p, manifest).catch((e) => - this.logService.error(`Error writing manifest for ${key}. ${e}`), - ); + await this.writeManifest(p, manifest); } else { - this.logService.warning(`${key} not found skipping.`); + this.logService.warning(`${key} not found, skipping.`); } } break; } case "linux": if (existsSync(`${this.homedir()}/.mozilla/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, firefoxJson, ); } if (existsSync(`${this.homedir()}/.config/google-chrome/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); } if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) { - // 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.writeManifest( + await this.writeManifest( `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, chromeJson, ); @@ -178,20 +217,23 @@ export class NativeMessagingMain { } } - generateDdgManifests() { + async generateDdgManifests() { const manifest = { name: "com.8bit.bitwarden", description: "Bitwarden desktop <-> DuckDuckGo bridge", path: this.binaryPath(), type: "stdio", }; + + if (!existsSync(manifest.path)) { + throw new Error(`Unable to find binary: ${manifest.path}`); + } + switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - this.writeManifest(path, manifest).catch((e) => - this.logService.error(`Error writing manifest for DuckDuckGo. ${e}`), - ); + await this.writeManifest(path, manifest); break; } default: @@ -199,86 +241,50 @@ export class NativeMessagingMain { } } - removeManifests() { + async removeManifests() { switch (process.platform) { - case "win32": - // 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 - fs.unlink(path.join(this.userPath, "browsers", "firefox.json")); - // 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 - fs.unlink(path.join(this.userPath, "browsers", "chrome.json")); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", - ); - // 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.deleteWindowsRegistry( - "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", - ); + case "win32": { + await this.removeIfExists(path.join(this.userPath, "browsers", "firefox.json")); + await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json")); + + const nmhs = this.getWindowsNMHS(); + for (const [, value] of Object.entries(nmhs)) { + await this.deleteWindowsRegistry(value); + } break; + } case "darwin": { const nmhs = this.getDarwinNMHS(); for (const [, value] of Object.entries(nmhs)) { - const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"); - if (existsSync(p)) { - // 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 - fs.unlink(p); - } + await this.removeIfExists( + path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), + ); } break; } - case "linux": - if ( - existsSync(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`) - ) { - // 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 - fs.unlink(`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`); - } - - if ( - existsSync( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } - - if ( - existsSync( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ) - ) { - // 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 - fs.unlink( - `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, - ); - } + case "linux": { + await this.removeIfExists( + `${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); + await this.removeIfExists( + `${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`, + ); break; + } default: break; } } - removeDdgManifests() { + async removeDdgManifests() { switch (process.platform) { case "darwin": { /* eslint-disable-next-line no-useless-escape */ const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`; - if (existsSync(path)) { - // 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 - fs.unlink(path); - } + await this.removeIfExists(path); break; } default: @@ -286,6 +292,16 @@ export class NativeMessagingMain { } } + private getWindowsNMHS() { + return { + Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden", + Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden", + Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden", + // Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well. + "Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden", + }; + } + private getDarwinNMHS() { /* eslint-disable no-useless-escape */ return { @@ -305,10 +321,13 @@ export class NativeMessagingMain { } private async writeManifest(destination: string, manifest: object) { + this.logService.debug(`Writing manifest: ${destination}`); + if (!existsSync(path.dirname(destination))) { await fs.mkdir(path.dirname(destination)); } - fs.writeFile(destination, JSON.stringify(manifest, null, 2)).catch(this.logService.error); + + await fs.writeFile(destination, JSON.stringify(manifest, null, 2)); } private binaryPath() { @@ -327,39 +346,26 @@ export class NativeMessagingMain { return regedit; } - private async createWindowsRegistry(check: string, location: string, jsonFile: string) { + private async createWindowsRegistry(location: string, jsonFile: string) { const regedit = this.getRegeditInstance(); - const list = util.promisify(regedit.list); const createKey = util.promisify(regedit.createKey); const putValue = util.promisify(regedit.putValue); this.logService.debug(`Adding registry: ${location}`); - // Check installed - try { - await list(check); - } catch { - this.logService.warning(`Not finding registry ${check} skipping.`); - return; - } + await createKey(location); - try { - await createKey(location); + // Insert path to manifest + const obj: any = {}; + obj[location] = { + default: { + value: jsonFile, + type: "REG_DEFAULT", + }, + }; - // Insert path to manifest - const obj: any = {}; - obj[location] = { - default: { - value: jsonFile, - type: "REG_DEFAULT", - }, - }; - - return putValue(obj); - } catch (error) { - this.logService.error(error); - } + return putValue(obj); } private async deleteWindowsRegistry(key: string) { @@ -385,4 +391,10 @@ export class NativeMessagingMain { return homedir(); } } + + private async removeIfExists(path: string) { + if (existsSync(path)) { + await fs.unlink(path); + } + } } diff --git a/apps/desktop/src/main/power-monitor.main.ts b/apps/desktop/src/main/power-monitor.main.ts index 067a380ba0..8cad5c1d9e 100644 --- a/apps/desktop/src/main/power-monitor.main.ts +++ b/apps/desktop/src/main/power-monitor.main.ts @@ -1,6 +1,7 @@ import { powerMonitor } from "electron"; -import { ElectronMainMessagingService } from "../services/electron-main-messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; + import { isSnapStore } from "../utils"; // tslint:disable-next-line @@ -10,7 +11,7 @@ const IdleCheckInterval = 30 * 1000; // 30 seconds export class PowerMonitorMain { private idle = false; - constructor(private messagingService: ElectronMainMessagingService) {} + constructor(private messagingService: MessageSender) {} init() { // ref: https://github.com/electron/electron/issues/13767 diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 11b38bd273..508c42fa72 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.5.0", "license": "GPL-3.0", "dependencies": { "@bitwarden/desktop-native": "file:../desktop_native", diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index a65dab016c..ea4b95491c 100644 --- a/apps/desktop/src/package.json +++ b/apps/desktop/src/package.json @@ -2,7 +2,7 @@ "name": "@bitwarden/desktop", "productName": "Bitwarden", "description": "A secure and free password manager for all of your devices.", - "version": "2024.4.2", + "version": "2024.5.0", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", diff --git a/apps/desktop/src/platform/preload.ts b/apps/desktop/src/platform/preload.ts index 1f6bd200e0..d81d647652 100644 --- a/apps/desktop/src/platform/preload.ts +++ b/apps/desktop/src/platform/preload.ts @@ -74,6 +74,13 @@ const nativeMessaging = { onMessage: (callback: (message: LegacyMessageWrapper | Message) => void) => { ipcRenderer.on("nativeMessaging", (_event, message) => callback(message)); }, + + manifests: { + generate: (create: boolean): Promise => + ipcRenderer.invoke("nativeMessaging.manifests", { create }), + generateDuckDuckGo: (create: boolean): Promise => + ipcRenderer.invoke("nativeMessaging.ddgManifests", { create }), + }, }; const crypto = { @@ -96,7 +103,8 @@ export default { isMacAppStore: isMacAppStore(), isWindowsStore: isWindowsStore(), reloadProcess: () => ipcRenderer.send("reload-process"), - log: (level: LogLevelType, message: string) => ipcRenderer.invoke("ipc.log", { level, message }), + log: (level: LogLevelType, message?: any, ...optionalParams: any[]) => + ipcRenderer.invoke("ipc.log", { level, message, optionalParams }), openContextMenu: ( menu: { @@ -117,12 +125,21 @@ export default { sendMessage: (message: { command: string } & any) => ipcRenderer.send("messagingService", message), - onMessage: (callback: (message: { command: string } & any) => void) => { - ipcRenderer.on("messagingService", (_event, message: any) => { - if (message.command) { - callback(message); - } - }); + onMessage: { + addListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.addListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, + removeListener: (callback: (message: { command: string } & any) => void) => { + ipcRenderer.removeListener("messagingService", (_event, message: any) => { + if (message.command) { + callback(message); + } + }); + }, }, launchUri: (uri: string) => ipcRenderer.invoke("launchUri", uri), diff --git a/apps/desktop/src/platform/services/desktop-settings.service.ts b/apps/desktop/src/platform/services/desktop-settings.service.ts index d967e5fb1d..09ddad07c1 100644 --- a/apps/desktop/src/platform/services/desktop-settings.service.ts +++ b/apps/desktop/src/platform/services/desktop-settings.service.ts @@ -164,7 +164,7 @@ export class DesktopSettingsService { /** * Sets the setting for whether or not the application should be shown in the dock. - * @param value `true` if the application should should in the dock, `false` if it should not. + * @param value `true` if the application should show in the dock, `false` if it should not. */ async setAlwaysShowDock(value: boolean) { await this.alwaysShowDockState.update(() => value); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts index 3d9171b52e..86463dccaa 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.spec.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.spec.ts @@ -1,6 +1,7 @@ import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { mock } from "jest-mock-extended"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -35,6 +36,7 @@ describe("electronCryptoService", () => { let accountService: FakeAccountService; let stateProvider: FakeStateProvider; const biometricStateService = mock(); + const kdfConfigService = mock(); const mockUserId = "mock user id" as UserId; @@ -54,6 +56,7 @@ describe("electronCryptoService", () => { accountService, stateProvider, biometricStateService, + kdfConfigService, ); }); diff --git a/apps/desktop/src/platform/services/electron-crypto.service.ts b/apps/desktop/src/platform/services/electron-crypto.service.ts index d113a18200..0ed0f73d41 100644 --- a/apps/desktop/src/platform/services/electron-crypto.service.ts +++ b/apps/desktop/src/platform/services/electron-crypto.service.ts @@ -1,6 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.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"; @@ -31,6 +32,7 @@ export class ElectronCryptoService extends CryptoService { accountService: AccountService, stateProvider: StateProvider, private biometricStateService: BiometricStateService, + kdfConfigService: KdfConfigService, ) { super( masterPasswordService, @@ -42,6 +44,7 @@ export class ElectronCryptoService extends CryptoService { stateService, accountService, stateProvider, + kdfConfigService, ); } diff --git a/apps/desktop/src/platform/services/electron-log.main.service.ts b/apps/desktop/src/platform/services/electron-log.main.service.ts index 832365785c..0725de3dc9 100644 --- a/apps/desktop/src/platform/services/electron-log.main.service.ts +++ b/apps/desktop/src/platform/services/electron-log.main.service.ts @@ -25,28 +25,28 @@ export class ElectronLogMainService extends BaseLogService { } log.initialize(); - ipcMain.handle("ipc.log", (_event, { level, message }) => { - this.write(level, message); + ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => { + this.write(level, message, ...optionalParams); }); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } switch (level) { case LogLevelType.Debug: - log.debug(message); + log.debug(message, ...optionalParams); break; case LogLevelType.Info: - log.info(message); + log.info(message, ...optionalParams); break; case LogLevelType.Warning: - log.warn(message); + log.warn(message, ...optionalParams); break; case LogLevelType.Error: - log.error(message); + log.error(message, ...optionalParams); break; default: break; diff --git a/apps/desktop/src/platform/services/electron-log.renderer.service.ts b/apps/desktop/src/platform/services/electron-log.renderer.service.ts index e0e0757e6a..cea939f160 100644 --- a/apps/desktop/src/platform/services/electron-log.renderer.service.ts +++ b/apps/desktop/src/platform/services/electron-log.renderer.service.ts @@ -6,27 +6,29 @@ export class ElectronLogRendererService extends BaseLogService { super(ipc.platform.isDev, filter); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } /* eslint-disable no-console */ - ipc.platform.log(level, message).catch((e) => console.log("Error logging", e)); + ipc.platform + .log(level, message, ...optionalParams) + .catch((e) => console.log("Error logging", e)); /* eslint-disable no-console */ switch (level) { case LogLevelType.Debug: - console.debug(message); + console.debug(message, ...optionalParams); break; case LogLevelType.Info: - console.info(message); + console.info(message, ...optionalParams); break; case LogLevelType.Warning: - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: - console.error(message); + console.error(message, ...optionalParams); break; default: break; diff --git a/apps/desktop/src/platform/services/electron-renderer-message.sender.ts b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts new file mode 100644 index 0000000000..037c303b3b --- /dev/null +++ b/apps/desktop/src/platform/services/electron-renderer-message.sender.ts @@ -0,0 +1,12 @@ +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; + +export class ElectronRendererMessageSender implements MessageSender { + send( + commandDefinition: CommandDefinition | string, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + ipc.platform.sendMessage(Object.assign({}, { command: command }, payload)); + } +} diff --git a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts b/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts deleted file mode 100644 index 192efc1dc6..0000000000 --- a/apps/desktop/src/platform/services/electron-renderer-messaging.service.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -export class ElectronRendererMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) { - ipc.platform.onMessage((message) => this.sendMessage(message.command, message, false)); - } - - send(subscriber: string, arg: any = {}) { - this.sendMessage(subscriber, arg, true); - } - - private sendMessage(subscriber: string, arg: any = {}, toMain: boolean) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - if (toMain) { - ipc.platform.sendMessage(message); - } - } -} diff --git a/apps/desktop/src/platform/utils/from-ipc-messaging.ts b/apps/desktop/src/platform/utils/from-ipc-messaging.ts new file mode 100644 index 0000000000..254a215ceb --- /dev/null +++ b/apps/desktop/src/platform/utils/from-ipc-messaging.ts @@ -0,0 +1,15 @@ +import { fromEventPattern, share } from "rxjs"; + +import { Message } from "@bitwarden/common/platform/messaging"; +import { tagAsExternal } from "@bitwarden/common/platform/messaging/internal"; + +/** + * Creates an observable that when subscribed to will listen to messaging events through IPC. + * @returns An observable stream of messages. + */ +export const fromIpcMessaging = () => { + return fromEventPattern>( + (handler) => ipc.platform.onMessage.addListener(handler), + (handler) => ipc.platform.onMessage.removeListener(handler), + ).pipe(tagAsExternal, share()); +}; diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss deleted file mode 100644 index c156456809..0000000000 --- a/apps/desktop/src/scss/plugins.scss +++ /dev/null @@ -1,95 +0,0 @@ -@import "~ngx-toastr/toastr"; - -@import "variables.scss"; - -.toast-container { - .toast-close-button { - @include themify($themes) { - color: themed("toastTextColor"); - } - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - @include themify($themes) { - color: themed("toastTextColor"); - } - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("dangerColor"); - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warningColor"); - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("infoColor"); - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("successColor"); - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 033a0f8b67..54c1385dcf 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -11,7 +11,6 @@ @import "buttons.scss"; @import "misc.scss"; @import "modal.scss"; -@import "plugins.scss"; @import "environment.scss"; @import "header.scss"; @import "left-nav.scss"; diff --git a/apps/desktop/src/services/electron-main-messaging.service.ts b/apps/desktop/src/services/electron-main-messaging.service.ts index 71e1b1d7d5..ce4ffd903a 100644 --- a/apps/desktop/src/services/electron-main-messaging.service.ts +++ b/apps/desktop/src/services/electron-main-messaging.service.ts @@ -2,18 +2,17 @@ import * as path from "path"; import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, Notification, shell } from "electron"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; +import { MessageSender, CommandDefinition } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Using implementation helper in implementation +import { getCommand } from "@bitwarden/common/platform/messaging/internal"; import { SafeUrls } from "@bitwarden/common/platform/misc/safe-urls"; import { WindowMain } from "../main/window.main"; import { RendererMenuItem } from "../utils"; -export class ElectronMainMessagingService implements MessagingService { - constructor( - private windowMain: WindowMain, - private onMessage: (message: any) => void, - ) { +export class ElectronMainMessagingService implements MessageSender { + constructor(private windowMain: WindowMain) { ipcMain.handle("appVersion", () => { return app.getVersion(); }); @@ -88,9 +87,9 @@ export class ElectronMainMessagingService implements MessagingService { }); } - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.onMessage(message); + send(commandDefinition: CommandDefinition | string, arg: T | object = {}) { + const command = getCommand(commandDefinition); + const message = Object.assign({}, { command: command }, arg); if (this.windowMain.win != null) { this.windowMain.win.webContents.send("messagingService", message); } diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index e38339d5ad..4512e175ce 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -1,12 +1,13 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -28,7 +29,7 @@ import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-me export class EncryptedMessageHandlerService { constructor( - private stateService: StateService, + private accountService: AccountService, private authService: AuthService, private cipherService: CipherService, private policyService: PolicyService, @@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService { } private async checkUserStatus(userId: string): Promise { - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (userId !== activeUserId) { return "not-active-user"; @@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService { } private async statusCommandHandler(): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); - const activeUserId = await this.stateService.getUserId(); + const accounts = await firstValueFrom(this.accountService.accounts$); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); if (!accounts || !Object.keys(accounts)) { return []; } return Promise.all( - Object.keys(accounts).map(async (userId) => { + Object.keys(accounts).map(async (userId: UserId) => { const authStatus = await this.authService.getAuthStatus(userId); - const email = await this.stateService.getEmail({ userId }); + const email = accounts[userId].email; return { id: userId, @@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService { } const ciphersResponse: CipherResponse[] = []; - const activeUserId = await this.stateService.getUserId(); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); const authStatus = await this.authService.getAuthStatus(activeUserId); if (authStatus !== AuthenticationStatus.Unlocked) { diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 01d9476977..48bdc60047 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -1,6 +1,7 @@ import { Injectable, NgZone } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -41,6 +42,7 @@ export class NativeMessagingService { private biometricStateService: BiometricStateService, private nativeMessageHandler: NativeMessageHandlerService, private dialogService: DialogService, + private accountService: AccountService, private ngZone: NgZone, ) {} @@ -51,9 +53,7 @@ export class NativeMessagingService { private async messageHandler(msg: LegacyMessageWrapper | Message) { const outerMessage = msg as Message; if (outerMessage.version) { - // 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.nativeMessageHandler.handleMessage(outerMessage); + await this.nativeMessageHandler.handleMessage(outerMessage); return; } @@ -64,7 +64,7 @@ export class NativeMessagingService { const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey); // Validate the UserId to ensure we are logged into the same account. - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); const userIds = Object.keys(accounts); if (!userIds.includes(rawMessage.userId)) { ipc.platform.nativeMessaging.sendMessage({ @@ -81,7 +81,7 @@ export class NativeMessagingService { }); const fingerprint = await this.cryptoService.getFingerprint( - await this.stateService.getUserId(), + rawMessage.userId, remotePublicKey, ); @@ -98,9 +98,7 @@ export class NativeMessagingService { } } - // 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.secureCommunication(remotePublicKey, appId); + await this.secureCommunication(remotePublicKey, appId); return; } @@ -144,9 +142,7 @@ export class NativeMessagingService { ? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$) : this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId); if (!(await biometricUnlockPromise)) { - // 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.send({ command: "biometricUnlock", response: "not enabled" }, appId); + await this.send({ command: "biometricUnlock", response: "not enabled" }, appId); return this.ngZone.run(() => this.dialogService.openSimpleDialog({ @@ -172,9 +168,7 @@ export class NativeMessagingService { // we send the master key still for backwards compatibility // with older browser extensions // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472) - // 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.send( + await this.send( { command: "biometricUnlock", response: "unlocked", @@ -184,14 +178,10 @@ export class NativeMessagingService { appId, ); } 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 - this.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } } catch (e) { - // 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.send({ command: "biometricUnlock", response: "canceled" }, appId); + await this.send({ command: "biometricUnlock", response: "canceled" }, appId); } break; diff --git a/apps/desktop/src/vault/app/vault/collections.component.ts b/apps/desktop/src/vault/app/vault/collections.component.ts index cd08427016..4b6a88f325 100644 --- a/apps/desktop/src/vault/app/vault/collections.component.ts +++ b/apps/desktop/src/vault/app/vault/collections.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -20,6 +21,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { platformUtilsService: PlatformUtilsService, organizationService: OrganizationService, logService: LogService, + configService: ConfigService, ) { super( collectionService, @@ -28,6 +30,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, ); } } diff --git a/apps/web/package.json b/apps/web/package.json index 55fe0987d7..6e5355c708 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.4.1", + "version": "2024.5.0", "scripts": { "build:oss": "webpack", "build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js", diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts index b1a84c22f3..7de0c98cd5 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.ts @@ -58,7 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html index 3afb816e14..5fcf7b0f42 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.html @@ -50,7 +50,12 @@ -

{{ "editGroupCollectionsDesc" | i18n }}

+

+ {{ "editGroupCollectionsDesc" | i18n }} + + {{ "editGroupCollectionsRestrictionsDesc" | i18n }} + +

- - ×
- - - -
- - + + + + + + + + + + + + + + + + + + + + + `; diff --git a/apps/web/src/app/billing/organizations/organization-plans.component.ts b/apps/web/src/app/billing/organizations/organization-plans.component.ts index 30691ce87d..23d48d93be 100644 --- a/apps/web/src/app/billing/organizations/organization-plans.component.ts +++ b/apps/web/src/app/billing/organizations/organization-plans.component.ts @@ -48,11 +48,7 @@ interface OnSuccessArgs { organizationId: string; } -const AllowedLegacyPlanTypes = [ - PlanType.TeamsMonthly2023, - PlanType.TeamsAnnually2023, - PlanType.EnterpriseAnnually2023, - PlanType.EnterpriseMonthly2023, +const Allowed2020PlansForLegacyProviders = [ PlanType.TeamsMonthly2020, PlanType.TeamsAnnually2020, PlanType.EnterpriseAnnually2020, @@ -283,7 +279,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (!this.currentPlan || this.currentPlan.upgradeSortOrder < plan.upgradeSortOrder) && (!this.hasProvider || plan.product !== ProductType.TeamsStarter) && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ); result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -298,7 +295,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { (plan) => plan.product === selectedProductType && ((!this.isProviderQualifiedFor2020Plan() && this.planIsEnabled(plan)) || - (this.isProviderQualifiedFor2020Plan() && AllowedLegacyPlanTypes.includes(plan.type))), + (this.isProviderQualifiedFor2020Plan() && + Allowed2020PlansForLegacyProviders.includes(plan.type))), ) || []; result.sort((planA, planB) => planA.displaySortOrder - planB.displaySortOrder); @@ -589,7 +587,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy { this.formPromise = doSubmit(); const organizationId = await this.formPromise; this.onSuccess.emit({ organizationId: organizationId }); - this.messagingService.send("organizationCreated", organizationId); + // TODO: No one actually listening to this message? + this.messagingService.send("organizationCreated", { organizationId }); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 16641c0d52..5a71e353d7 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -1,6 +1,6 @@ - + {{ "loading" | i18n }} @@ -256,3 +256,13 @@ + +
+ + {{ + "manageBillingFromProviderPortalMessage" | i18n + }} +
+
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 9326359bd8..198332db0c 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 @@ -5,7 +5,8 @@ import { concatMap, firstValueFrom, lastValueFrom, Observable, Subject, takeUnti import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationApiKeyType } from "@bitwarden/common/admin-console/enums"; +import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; +import { OrganizationApiKeyType, ProviderType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { PlanType } from "@bitwarden/common/billing/enums"; import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response"; @@ -28,6 +29,7 @@ import { } from "../shared/offboarding-survey.component"; import { BillingSyncApiKeyComponent } from "./billing-sync-api-key.component"; +import { ManageBilling } from "./icons/manage-billing.icon"; import { SecretsManagerSubscriptionOptions } from "./sm-adjust-subscription.component"; @Component({ @@ -47,11 +49,17 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy loading: boolean; locale: string; showUpdatedSubscriptionStatusSection$: Observable; + manageBillingFromProviderPortal = ManageBilling; + isProviderManaged = false; protected readonly teamsStarter = ProductType.TeamsStarter; private destroy$ = new Subject(); + protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( + FeatureFlag.EnableConsolidatedBilling, + ); + constructor( private apiService: ApiService, private platformUtilsService: PlatformUtilsService, @@ -62,6 +70,7 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy private route: ActivatedRoute, private dialogService: DialogService, private configService: ConfigService, + private providerService: ProviderApiServiceAbstraction, ) {} async ngOnInit() { @@ -84,7 +93,6 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.showUpdatedSubscriptionStatusSection$ = this.configService.getFeatureFlag$( FeatureFlag.AC1795_UpdatedSubscriptionStatusSection, - false, ); } @@ -100,6 +108,12 @@ export class OrganizationSubscriptionCloudComponent implements OnInit, OnDestroy this.loading = true; this.locale = await firstValueFrom(this.i18nService.locale$); this.userOrg = await this.organizationService.get(this.organizationId); + if (this.userOrg.hasProvider) { + const provider = await this.providerService.getProvider(this.userOrg.providerId); + const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); + this.isProviderManaged = provider.type == ProviderType.Msp && enableConsolidatedBilling; + } + if (this.userOrg.canViewSubscription) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); this.lineItems = this.sub?.subscription?.items; diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 6d6691f336..b4c1224db9 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -42,7 +42,10 @@ : subscription.expirationWithGracePeriod ) | date: "mediumDate" }} -
+
{{ "selfHostGracePeriodHelp" | i18n: (subscription.expirationWithGracePeriod | date: "mediumDate") diff --git a/apps/web/src/app/core/broadcaster-messaging.service.ts b/apps/web/src/app/core/broadcaster-messaging.service.ts deleted file mode 100644 index 7c8e4eef43..0000000000 --- a/apps/web/src/app/core/broadcaster-messaging.service.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; - -@Injectable() -export class BroadcasterMessagingService implements MessagingService { - constructor(private broadcasterService: BroadcasterService) {} - - send(subscriber: string, arg: any = {}) { - const message = Object.assign({}, { command: subscriber }, arg); - this.broadcasterService.send(message); - } -} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 9d53bc39f0..c60280014c 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -5,7 +5,6 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa import { SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, LOCALES_DIRECTORY, SYSTEM_LANGUAGE, MEMORY_STORAGE, @@ -14,15 +13,16 @@ import { OBSERVABLE_DISK_LOCAL_STORAGE, WINDOW, SafeInjectionToken, + CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { ClientType } from "@bitwarden/common/enums"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; @@ -51,7 +51,6 @@ import { WebStorageServiceProvider } from "../platform/web-storage-service.provi import { WindowStorageService } from "../platform/window-storage.service"; import { CollectionAdminService } from "../vault/core/collection-admin.service"; -import { BroadcasterMessagingService } from "./broadcaster-messaging.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; import { ModalService } from "./modal.service"; @@ -80,10 +79,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: false, - }), safeProvider({ provide: I18nServiceAbstraction, useClass: I18nService, @@ -117,11 +112,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebPlatformUtilsService, useAngularDecorators: true, }), - safeProvider({ - provide: MessagingServiceAbstraction, - useClass: BroadcasterMessagingService, - useAngularDecorators: true, - }), safeProvider({ provide: ModalServiceAbstraction, useClass: ModalService, @@ -169,6 +159,10 @@ const safeProviders: SafeProvider[] = [ new DefaultThemeStateService(globalStateProvider, ThemeType.Light), deps: [GlobalStateProvider], }), + safeProvider({ + provide: CLIENT_TYPE, + useValue: ClientType.Web, + }), ]; @NgModule({ diff --git a/apps/web/src/app/core/init.service.ts b/apps/web/src/app/core/init.service.ts index d5576d3bf7..55dc1544ff 100644 --- a/apps/web/src/app/core/init.service.ts +++ b/apps/web/src/app/core/init.service.ts @@ -1,16 +1,19 @@ import { DOCUMENT } from "@angular/common"; import { Inject, Injectable } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { CryptoService as CryptoServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service"; import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { ContainerService } from "@bitwarden/common/platform/services/container.service"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; @@ -27,6 +30,8 @@ export class InitService { private cryptoService: CryptoServiceAbstraction, private themingService: AbstractThemingService, private encryptService: EncryptService, + private userAutoUnlockKeyService: UserAutoUnlockKeyService, + private accountService: AccountService, @Inject(DOCUMENT) private document: Document, ) {} @@ -34,6 +39,13 @@ export class InitService { return async () => { await this.stateService.init(); + const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + if (activeAccount) { + // If there is an active account, we must await the process of setting the user key in memory + // if the auto user key is set to avoid race conditions of any code trying to access the user key from mem. + await this.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(activeAccount.id); + } + setTimeout(() => this.notificationsService.init(), 3000); await this.vaultTimeoutService.init(true); await this.i18nService.init(); diff --git a/apps/web/src/app/core/state/state.service.ts b/apps/web/src/app/core/state/state.service.ts index 1ae62d8591..de47a69555 100644 --- a/apps/web/src/app/core/state/state.service.ts +++ b/apps/web/src/app/core/state/state.service.ts @@ -4,16 +4,12 @@ import { MEMORY_STORAGE, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, } from "@bitwarden/angular/services/injection-tokens"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "@bitwarden/common/platform/abstractions/storage.service"; +import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; import { StorageOptions } from "@bitwarden/common/platform/models/domain/storage-options"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; @@ -27,14 +23,13 @@ export class StateService extends BaseStateService { constructor( storageService: AbstractStorageService, @Inject(SECURE_STORAGE) secureStorageService: AbstractStorageService, - @Inject(MEMORY_STORAGE) memoryStorageService: AbstractMemoryStorageService, + @Inject(MEMORY_STORAGE) memoryStorageService: AbstractStorageService, logService: LogService, @Inject(STATE_FACTORY) stateFactory: StateFactory, accountService: AccountService, environmentService: EnvironmentService, tokenService: TokenService, migrationRunner: MigrationRunner, - @Inject(STATE_SERVICE_USE_CACHE) useAccountCache = true, ) { super( storageService, @@ -46,7 +41,6 @@ export class StateService extends BaseStateService { environmentService, tokenService, migrationRunner, - useAccountCache, ); } diff --git a/apps/web/src/app/layouts/header/web-header.component.html b/apps/web/src/app/layouts/header/web-header.component.html index e24013de6f..e2b3e7910a 100644 --- a/apps/web/src/app/layouts/header/web-header.component.html +++ b/apps/web/src/app/layouts/header/web-header.component.html @@ -58,7 +58,7 @@ [bitMenuTriggerFor]="accountMenu" class="tw-border-0 tw-bg-transparent tw-p-0" > - + @@ -67,7 +67,7 @@ class="tw-flex tw-items-center tw-px-4 tw-py-1 tw-leading-tight tw-text-info" appStopProp > - +
{{ "loggedInAs" | i18n }} diff --git a/apps/web/src/app/layouts/header/web-header.component.ts b/apps/web/src/app/layouts/header/web-header.component.ts index 1f012e52dd..9906bd53ba 100644 --- a/apps/web/src/app/layouts/header/web-header.component.ts +++ b/apps/web/src/app/layouts/header/web-header.component.ts @@ -1,16 +1,17 @@ import { Component, Input } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, map, Observable } from "rxjs"; +import { map, Observable } from "rxjs"; +import { User } from "@bitwarden/angular/pipes/user-name.pipe"; import { UnassignedItemsBannerService } from "@bitwarden/angular/services/unassigned-items-banner.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AccountProfile } from "@bitwarden/common/platform/models/domain/account"; +import { UserId } from "@bitwarden/common/types/guid"; @Component({ selector: "app-header", @@ -28,7 +29,7 @@ export class WebHeaderComponent { @Input() icon: string; protected routeData$: Observable<{ titleId: string }>; - protected account$: Observable; + protected account$: Observable; protected canLock$: Observable; protected selfHosted: boolean; protected hostname = location.hostname; @@ -38,12 +39,12 @@ export class WebHeaderComponent { constructor( private route: ActivatedRoute, - private stateService: StateService, private platformUtilsService: PlatformUtilsService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private messagingService: MessagingService, protected unassignedItemsBannerService: UnassignedItemsBannerService, private configService: ConfigService, + private accountService: AccountService, ) { this.routeData$ = this.route.data.pipe( map((params) => { @@ -55,14 +56,7 @@ export class WebHeaderComponent { this.selfHosted = this.platformUtilsService.isSelfHost(); - this.account$ = combineLatest([ - this.stateService.activeAccount$, - this.stateService.accounts$, - ]).pipe( - map(([activeAccount, accounts]) => { - return accounts[activeAccount]?.profile; - }), - ); + this.account$ = this.accountService.activeAccount$; this.canLock$ = this.vaultTimeoutSettingsService .availableVaultTimeoutActions$() .pipe(map((actions) => actions.includes(VaultTimeoutAction.Lock))); diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index f038fafecc..62d8b6a075 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -21,9 +21,10 @@ ariaCurrentWhenActive="page" > - {{ - product.name - }} + {{ product.name }} diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts index 0ce98948c9..9495894432 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.ts @@ -1,12 +1,13 @@ import { Component, ViewChild } from "@angular/core"; -import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, concatMap } from "rxjs"; +import { ActivatedRoute, ParamMap, Router } from "@angular/router"; +import { combineLatest, concatMap, map } from "rxjs"; import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { MenuComponent } from "@bitwarden/components"; type ProductSwitcherItem = { @@ -48,6 +49,13 @@ export class ProductSwitcherContentComponent { this.organizationService.organizations$, this.route.paramMap, ]).pipe( + map(([orgs, paramMap]): [Organization[], ParamMap] => { + return [ + // Sort orgs by name to match the order within the sidebar + orgs.sort((a, b) => a.name.localeCompare(b.name)), + paramMap, + ]; + }), concatMap(async ([orgs, paramMap]) => { const routeOrg = orgs.find((o) => o.id === paramMap.get("organizationId")); // If the active route org doesn't have access to SM, find the first org that does. @@ -89,7 +97,7 @@ export class ProductSwitcherContentComponent { }, ac: { name: "Admin Console", - icon: "bwi-business", + icon: "bwi-user-monitor", appRoute: ["/organizations", acOrg?.id], marketingRoute: "https://bitwarden.com/products/business/", isActive: this.router.url.includes("/organizations/"), diff --git a/apps/web/src/app/layouts/user-layout.component.ts b/apps/web/src/app/layouts/user-layout.component.ts index fea0352867..eb507bd997 100644 --- a/apps/web/src/app/layouts/user-layout.component.ts +++ b/apps/web/src/app/layouts/user-layout.component.ts @@ -40,7 +40,6 @@ export class UserLayoutComponent implements OnInit { protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( FeatureFlag.ShowPaymentMethodWarningBanners, - false, ); constructor( diff --git a/apps/web/src/app/platform/web-migration-runner.ts b/apps/web/src/app/platform/web-migration-runner.ts index 4bd1d2d4b5..392eeeae04 100644 --- a/apps/web/src/app/platform/web-migration-runner.ts +++ b/apps/web/src/app/platform/web-migration-runner.ts @@ -1,3 +1,4 @@ +import { ClientType } from "@bitwarden/common/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -14,7 +15,7 @@ export class WebMigrationRunner extends MigrationRunner { migrationBuilderService: MigrationBuilderService, private diskLocalStorage: WindowStorageService, ) { - super(diskStorage, logService, migrationBuilderService); + super(diskStorage, logService, migrationBuilderService, ClientType.Web); } override async run(): Promise { @@ -46,7 +47,7 @@ class WebMigrationHelper extends MigrationHelper { storageService: WindowStorageService, logService: LogService, ) { - super(currentVersion, storageService, logService, "web-disk-local"); + super(currentVersion, storageService, logService, "web-disk-local", ClientType.Web); this.diskLocalStorageService = storageService; } diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 7c0b8c448e..33ec58bc61 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -1,6 +1,9 @@ import { NgModule } from "@angular/core"; -import { PasswordCalloutComponent } from "@bitwarden/auth/angular"; +import { + PasswordCalloutComponent, + UserVerificationFormInputComponent, +} from "@bitwarden/auth/angular"; import { LayoutComponent, NavigationModule } from "@bitwarden/components"; import { OrganizationLayoutComponent } from "../admin-console/organizations/layouts/organization-layout.component"; @@ -107,6 +110,7 @@ import { SharedModule } from "./shared.module"; OrganizationBadgeModule, PipesModule, PasswordCalloutComponent, + UserVerificationFormInputComponent, DangerZoneComponent, LayoutComponent, NavigationModule, diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index bc775f07e2..1b04583a39 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -4,7 +4,6 @@ import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; import { RouterModule } from "@angular/router"; import { InfiniteScrollModule } from "ngx-infinite-scroll"; -import { ToastrModule } from "ngx-toastr"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { @@ -52,7 +51,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library modules @@ -90,7 +88,6 @@ import "./locales"; ReactiveFormsModule, InfiniteScrollModule, RouterModule, - ToastrModule, JslibModule, // Component library diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index 0d67b7a769..4e63dd5cc9 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,9 +1,12 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; -import { Observable } from "rxjs"; +import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; +import { BehaviorSubject, Observable, Subject, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -12,27 +15,113 @@ import { AddEditComponent } from "../../../vault/individual-vault/add-edit.compo import { AddEditComponent as OrgAddEditComponent } from "../../../vault/org-vault/add-edit.component"; @Directive() -export class CipherReportComponent { +export class CipherReportComponent implements OnDestroy { @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) cipherAddEditModalRef: ViewContainerRef; + isAdminConsoleActive = false; loading = false; hasLoaded = false; ciphers: CipherView[] = []; + allCiphers: CipherView[] = []; organization: Organization; + organizations: Organization[]; organizations$: Observable; + filterStatus: any = [0]; + showFilterToggle: boolean = false; + vaultMsg: string = "vault"; + currentFilterStatus: number | string; + protected filterOrgStatus$ = new BehaviorSubject(0); + private destroyed$: Subject = new Subject(); + constructor( + protected cipherService: CipherService, private modalService: ModalService, protected passwordRepromptService: PasswordRepromptService, protected organizationService: OrganizationService, + protected i18nService: I18nService, + private syncService: SyncService, ) { this.organizations$ = this.organizationService.organizations$; + this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { + this.organizations = orgs; + }); + } + + ngOnDestroy(): void { + this.destroyed$.next(); + this.destroyed$.complete(); + } + + getName(filterId: string | number) { + let orgName: any; + + if (filterId === 0) { + orgName = this.i18nService.t("all"); + } else if (filterId === 1) { + orgName = this.i18nService.t("me"); + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgName = org.name; + return org; + } + }); + } + return orgName; + } + + getCount(filterId: string | number) { + let orgFilterStatus: any; + let cipherCount; + + if (filterId === 0) { + cipherCount = this.allCiphers.length; + } else if (filterId === 1) { + cipherCount = this.allCiphers.filter((c: any) => c.orgFilterStatus === null).length; + } else { + this.organizations.filter((org: Organization) => { + if (org.id === filterId) { + orgFilterStatus = org.id; + return org; + } + }); + cipherCount = this.allCiphers.filter( + (c: any) => c.orgFilterStatus === orgFilterStatus, + ).length; + } + return cipherCount; + } + + async filterOrgToggle(status: any) { + this.currentFilterStatus = status; + await this.setCiphers(); + if (status === 0) { + return; + } else if (status === 1) { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus == null); + } else { + this.ciphers = this.ciphers.filter((c: any) => c.orgFilterStatus === status); + } } async load() { this.loading = true; - await this.setCiphers(); + await this.syncService.fullSync(false); + // when a user fixes an item in a report we want to persist the filter they had + // if they fix the last item of that filter we will go back to the "All" filter + if (this.currentFilterStatus) { + if (this.ciphers.length > 2) { + this.filterOrgStatus$.next(this.currentFilterStatus); + await this.filterOrgToggle(this.currentFilterStatus); + } else { + this.filterOrgStatus$.next(0); + await this.filterOrgToggle(0); + } + } else { + await this.setCiphers(); + } this.loading = false; this.hasLoaded = true; } @@ -76,7 +165,7 @@ export class CipherReportComponent { } protected async setCiphers() { - this.ciphers = []; + this.allCiphers = []; } protected async repromptCipher(c: CipherView) { @@ -85,4 +174,32 @@ export class CipherReportComponent { (await this.passwordRepromptService.showPasswordPrompt()) ); } + + protected async getAllCiphers(): Promise { + return await this.cipherService.getAllDecrypted(); + } + + protected filterCiphersByOrg(ciphersList: CipherView[]) { + this.allCiphers = [...ciphersList]; + + this.ciphers = ciphersList.map((ciph: any) => { + ciph.orgFilterStatus = ciph.organizationId; + + if (this.filterStatus.indexOf(ciph.organizationId) === -1 && ciph.organizationId != null) { + this.filterStatus.push(ciph.organizationId); + } else if (this.filterStatus.indexOf(1) === -1 && ciph.organizationId == null) { + this.filterStatus.splice(1, 0, 1); + } + return ciph; + }); + + if (this.filterStatus.length > 2) { + this.showFilterToggle = true; + this.vaultMsg = "vaults"; + } else { + // If a user fixes an item and there is only one item left remove the filter toggle and change the vault message to singular + this.showFilterToggle = false; + this.vaultMsg = "vault"; + } + } } diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html index af80e2e62b..30801a42fd 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.html @@ -11,9 +11,32 @@ - {{ "exposedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "exposedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index d1cf89eb08..07dc218bd6 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -8,6 +9,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { ExposedPasswordsReportComponent } from "./exposed-passwords-report.component"; @@ -17,9 +19,14 @@ describe("ExposedPasswordsReportComponent", () => { let component: ExposedPasswordsReportComponent; let fixture: ComponentFixture; let auditService: MockProxy; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; beforeEach(() => { + syncServiceMock = mock(); auditService = mock(); + organizationService = mock(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -35,7 +42,7 @@ describe("ExposedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, @@ -45,6 +52,10 @@ describe("ExposedPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock(), @@ -78,4 +89,8 @@ describe("ExposedPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts index 631e9ef8a8..cabc7bdfa1 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.ts @@ -3,7 +3,9 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -24,8 +26,17 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, ) { - super(modalService, passwordRepromptService, organizationService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { @@ -36,7 +47,9 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple const allCiphers = await this.getAllCiphers(); const exposedPasswordCiphers: CipherView[] = []; const promises: Promise[] = []; - allCiphers.forEach((ciph) => { + this.filterStatus = [0]; + + allCiphers.forEach((ciph: any) => { const { type, login, isDeleted, edit, viewPassword, id } = ciph; if ( type !== CipherType.Login || @@ -48,6 +61,7 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple ) { return; } + const promise = this.auditService.passwordLeaked(login.password).then((exposedCount) => { if (exposedCount > 0) { exposedPasswordCiphers.push(ciph); @@ -57,11 +71,8 @@ export class ExposedPasswordsReportComponent extends CipherReportComponent imple promises.push(promise); }); await Promise.all(promises); - this.ciphers = [...exposedPasswordCiphers]; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(exposedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html index d81fc2d413..ae03a3bcb8 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.html @@ -16,9 +16,32 @@ - {{ "inactive2faFoundDesc" | i18n: (ciphers.length | number) }} + {{ "inactive2faFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 97321480fa..80760eb5de 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -8,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { InactiveTwoFactorReportComponent } from "./inactive-two-factor-report.component"; @@ -16,8 +18,13 @@ import { cipherData } from "./reports-ciphers.mock"; describe("InactiveTwoFactorReportComponent", () => { let component: InactiveTwoFactorReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); + syncServiceMock = mock(); // 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 TestBed.configureTestingModule({ @@ -29,7 +36,7 @@ describe("InactiveTwoFactorReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, @@ -43,6 +50,10 @@ describe("InactiveTwoFactorReportComponent", () => { provide: PasswordRepromptService, useValue: mock(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock(), @@ -83,4 +94,8 @@ describe("InactiveTwoFactorReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts index 15b79981b6..5cfe2cd1a9 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.ts @@ -2,9 +2,11 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -26,8 +28,17 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl modalService: ModalService, private logService: LogService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, ) { - super(modalService, passwordRepromptService, organizationService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { @@ -45,6 +56,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl const allCiphers = await this.getAllCiphers(); const inactive2faCiphers: CipherView[] = []; const docs = new Map(); + this.filterStatus = [0]; allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, id, viewPassword } = ciph; @@ -58,6 +70,7 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl ) { return; } + for (let i = 0; i < login.uris.length; i++) { const u = login.uris[i]; if (u.uri != null && u.uri !== "") { @@ -75,15 +88,12 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl } } }); - this.ciphers = [...inactive2faCiphers]; + + this.filterCiphersByOrg(inactive2faCiphers); this.cipherDocs = docs; } } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); - } - private async load2fa() { if (this.services.size > 0) { return; diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html index cde2e59ea8..549773ba8c 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.html @@ -16,9 +16,34 @@ - {{ "reusedPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "reusedPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 450e42805a..9d16bbb1c6 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -1,12 +1,14 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -15,8 +17,13 @@ import { ReusedPasswordsReportComponent } from "./reused-passwords-report.compon describe("ReusedPasswordsReportComponent", () => { let component: ReusedPasswordsReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); + syncServiceMock = mock(); // 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 TestBed.configureTestingModule({ @@ -28,7 +35,7 @@ describe("ReusedPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, @@ -38,6 +45,10 @@ describe("ReusedPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock(), @@ -69,4 +80,8 @@ describe("ReusedPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts index f785186c15..70cb2ed69b 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.ts @@ -2,7 +2,9 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; @@ -22,8 +24,17 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, ) { - super(modalService, passwordRepromptService, organizationService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { @@ -34,6 +45,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem const allCiphers = await this.getAllCiphers(); const ciphersWithPasswords: CipherView[] = []; this.passwordUseMap = new Map(); + this.filterStatus = [0]; + allCiphers.forEach((ciph) => { const { type, login, isDeleted, edit, viewPassword } = ciph; if ( @@ -46,6 +59,7 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem ) { return; } + ciphersWithPasswords.push(ciph); if (this.passwordUseMap.has(login.password)) { this.passwordUseMap.set(login.password, this.passwordUseMap.get(login.password) + 1); @@ -57,11 +71,8 @@ export class ReusedPasswordsReportComponent extends CipherReportComponent implem (c) => this.passwordUseMap.has(c.login.password) && this.passwordUseMap.get(c.login.password) > 1, ); - this.ciphers = reusedPasswordCiphers; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(reusedPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html index 616bdbba0b..ced0ff9731 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.html @@ -16,9 +16,33 @@ - {{ "unsecuredWebsitesFoundDesc" | i18n: (ciphers.length | number) }} + {{ "unsecuredWebsitesFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + +
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 5cdf640c55..e616d1f21e 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -1,12 +1,14 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { mock } from "jest-mock-extended"; +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -15,8 +17,13 @@ import { UnsecuredWebsitesReportComponent } from "./unsecured-websites-report.co describe("UnsecuredWebsitesReportComponent", () => { let component: UnsecuredWebsitesReportComponent; let fixture: ComponentFixture; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; beforeEach(() => { + organizationService = mock(); + organizationService.organizations$ = of([]); + syncServiceMock = mock(); // 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 TestBed.configureTestingModule({ @@ -28,7 +35,7 @@ describe("UnsecuredWebsitesReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, @@ -38,6 +45,10 @@ describe("UnsecuredWebsitesReportComponent", () => { provide: PasswordRepromptService, useValue: mock(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock(), @@ -69,4 +80,8 @@ describe("UnsecuredWebsitesReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts index 2de70e928b..0a8023c303 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.ts @@ -2,9 +2,10 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { PasswordRepromptService } from "@bitwarden/vault"; import { CipherReportComponent } from "./cipher-report.component"; @@ -21,8 +22,17 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, ) { - super(modalService, passwordRepromptService, organizationService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { @@ -31,18 +41,15 @@ export class UnsecuredWebsitesReportComponent extends CipherReportComponent impl async setCiphers() { const allCiphers = await this.getAllCiphers(); + this.filterStatus = [0]; const unsecuredCiphers = allCiphers.filter((c) => { if (c.type !== CipherType.Login || !c.login.hasUris || c.isDeleted) { return false; } - return c.login.uris.some((u) => u.uri != null && u.uri.indexOf("http://") === 0); - }); - this.ciphers = unsecuredCiphers.filter( - (c) => (!this.organization && c.edit) || (this.organization && !c.edit), - ); - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + return c.login.uris.some((u: any) => u.uri != null && u.uri.indexOf("http://") === 0); + }); + + this.filterCiphersByOrg(unsecuredCiphers); } } diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html index b4c77b2fa1..a943c8c29e 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html @@ -16,9 +16,32 @@ - {{ "weakPasswordsFoundDesc" | i18n: (ciphers.length | number) }} + {{ "weakPasswordsFoundReportDesc" | i18n: (ciphers.length | number) : vaultMsg }} + + + + {{ getName(status) }} + {{ getCount(status) }} + + + + + + + + + + diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 666bec7a1a..4a9667f8b8 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -6,6 +6,7 @@ import { CollectionView } from "@bitwarden/common/vault/models/view/collection.v import { GroupView } from "../../../admin-console/organizations/core"; import { CollectionAdminView } from "../../core/views/collection-admin.view"; +import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { convertToPermission, @@ -20,6 +21,7 @@ import { RowHeightClass } from "./vault-items.component"; }) export class VaultCollectionRowComponent { protected RowHeightClass = RowHeightClass; + protected Unassigned = "unassigned"; @Input() disabled: boolean; @Input() collection: CollectionView; @@ -52,8 +54,8 @@ export class VaultCollectionRowComponent { } get permissionText() { - if (!(this.collection as CollectionAdminView).assigned) { - return "-"; + if (this.collection.id != Unassigned && !(this.collection as CollectionAdminView).assigned) { + return this.i18nService.t("noAccess"); } else { const permissionList = getPermissionList(this.organization?.flexibleCollections); return this.i18nService.t( @@ -62,6 +64,13 @@ export class VaultCollectionRowComponent { } } + get permissionTooltip() { + if (this.collection.id == Unassigned) { + return this.i18nService.t("collectionAdminConsoleManaged"); + } + return ""; + } + protected edit() { this.onEvent.next({ type: "editCollection", item: this.collection }); } diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html index c63273fabd..ba69c038fb 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html @@ -99,8 +99,12 @@ (checkedToggled)="selection.toggle(item)" (onEvent)="event($event)" > + o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user without edit access should not see the Edit option unless that user has "Can Manage" access to a collection + if ( + !collection.manage && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.editAnyCollection + ) { + return false; + } + //Owner/Admin and Custom Users with Edit can see Edit and Access of Orphaned Collections + if ( + collection.addAccess && + collection.id !== Unassigned && + ((organization?.type === OrganizationUserType.Custom && + organization?.permissions.editAnyCollection) || + organization.isAdmin || + organization.isOwner) + ) { + return true; + } + } return collection.canEdit(organization, this.flexibleCollectionsV1Enabled); } @@ -112,6 +136,32 @@ export class VaultItemsComponent { } const organization = this.allOrganizations.find((o) => o.id === collection.organizationId); + + if (this.flexibleCollectionsV1Enabled) { + //Custom user with only edit access should not see the Delete button for orphaned collections + if ( + collection.addAccess && + organization?.type === OrganizationUserType.Custom && + !organization?.permissions.deleteAnyCollection && + organization?.permissions.editAnyCollection + ) { + return false; + } + + // Owner/Admin with no access to a collection will not see Delete + if ( + !collection.assigned && + !collection.addAccess && + (organization.isAdmin || organization.isOwner) && + !( + organization?.type === OrganizationUserType.Custom && + organization?.permissions.deleteAnyCollection + ) + ) { + return false; + } + } + return collection.canDelete(organization); } @@ -160,10 +210,27 @@ export class VaultItemsComponent { } protected canClone(vaultItem: VaultItem) { - return ( - (vaultItem.cipher.organizationId && this.cloneableOrganizationCiphers) || - vaultItem.cipher.organizationId == null - ); + if (vaultItem.cipher.organizationId == null) { + return true; + } + + const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId); + + // Admins and custom users can always clone in the Org Vault + if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) { + return true; + } + + // Check if the cipher belongs to a collection with canManage permission + const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id); + + for (const collection of orgCollections) { + if (vaultItem.cipher.collectionIds.includes(collection.id) && collection.manage) { + return true; + } + } + + return false; } private refreshItems() { diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index ad80c9f4e5..41aa766e3a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -55,7 +55,6 @@ export default { { provide: StateService, useValue: { - activeAccount$: new BehaviorSubject("1").asObservable(), accounts$: new BehaviorSubject({ "1": { profile: { name: "Foo" } } }).asObservable(), async getShowFavicon() { return true; diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/apps/web/src/app/vault/core/views/collection-admin.view.ts index d942d42fb8..cc217fc9ce 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/apps/web/src/app/vault/core/views/collection-admin.view.ts @@ -1,3 +1,4 @@ +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; @@ -7,6 +8,7 @@ import { CollectionAccessSelectionView } from "../../../admin-console/organizati export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; users: CollectionAccessSelectionView[] = []; + addAccess: boolean; /** * Flag indicating the user has been explicitly assigned to this Collection @@ -31,6 +33,36 @@ export class CollectionAdminView extends CollectionView { this.assigned = response.assigned; } + groupsCanManage() { + if (this.groups.length === 0) { + return this.groups; + } + + const returnedGroups = this.groups.filter((group) => { + if (group.manage) { + return group; + } + }); + return returnedGroups; + } + + usersCanManage(revokedUsers: OrganizationUserUserDetailsResponse[]) { + if (this.users.length === 0) { + return this.users; + } + + const returnedUsers = this.users.filter((user) => { + const isRevoked = revokedUsers.some((revoked) => revoked.id === user.id); + if (user.manage && !isRevoked) { + return user; + } + }); + return returnedUsers; + } + + /** + * Whether the current user can edit the collection, including user and group access + */ override canEdit(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { return org?.flexibleCollections ? org?.canEditAnyCollection(flexibleCollectionsV1Enabled) || this.manage @@ -43,4 +75,18 @@ export class CollectionAdminView extends CollectionView { ? org?.canDeleteAnyCollection || (!org?.limitCollectionCreationDeletion && this.manage) : org?.canDeleteAnyCollection || (org?.canDeleteAssignedCollections && this.assigned); } + + /** + * Whether the user can modify user access to this collection + */ + canEditUserAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { + return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageUsers; + } + + /** + * Whether the user can modify group access to this collection + */ + canEditGroupAccess(org: Organization, flexibleCollectionsV1Enabled: boolean): boolean { + return this.canEdit(org, flexibleCollectionsV1Enabled) || org.permissions.manageGroups; + } } diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index 4050823a6d..f49c54ac32 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -1,4 +1,4 @@ -import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; import { firstValueFrom } from "rxjs"; @@ -54,7 +54,10 @@ export class BulkDeleteDialogComponent { private flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, + ); + + private restrictProviderAccess$ = this.configService.getFeatureFlag$( + FeatureFlag.RestrictProviderAccess, ); constructor( @@ -82,10 +85,11 @@ export class BulkDeleteDialogComponent { const deletePromises: Promise[] = []; if (this.cipherIds.length) { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); if ( !this.organization || - !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled) + !this.organization.canEditAllCiphers(flexibleCollectionsV1Enabled, restrictProviderAccess) ) { deletePromises.push(this.deleteCiphers()); } else { @@ -119,7 +123,11 @@ export class BulkDeleteDialogComponent { private async deleteCiphers(): Promise { const flexibleCollectionsV1Enabled = await firstValueFrom(this.flexibleCollectionsV1Enabled$); - const asAdmin = this.organization?.canEditAllCiphers(flexibleCollectionsV1Enabled); + const restrictProviderAccess = await firstValueFrom(this.restrictProviderAccess$); + const asAdmin = this.organization?.canEditAllCiphers( + flexibleCollectionsV1Enabled, + restrictProviderAccess, + ); if (this.permanent) { await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); } else { diff --git a/apps/web/src/app/vault/individual-vault/collections.component.html b/apps/web/src/app/vault/individual-vault/collections.component.html index 5adf9c4e58..d9c2145f0b 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.html +++ b/apps/web/src/app/vault/individual-vault/collections.component.html @@ -32,7 +32,13 @@ [(ngModel)]="$any(c).checked" name="Collection[{{ i }}].Checked" appStopProp - [disabled]="!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)" + [disabled]=" + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess + ) + " /> {{ c.name }} diff --git a/apps/web/src/app/vault/individual-vault/collections.component.ts b/apps/web/src/app/vault/individual-vault/collections.component.ts index 6add775b4a..af9c3476bd 100644 --- a/apps/web/src/app/vault/individual-vault/collections.component.ts +++ b/apps/web/src/app/vault/individual-vault/collections.component.ts @@ -1,8 +1,9 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { Component, OnDestroy, Inject } from "@angular/core"; +import { Component, Inject, OnDestroy } from "@angular/core"; import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -23,6 +24,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService: CipherService, organizationSerivce: OrganizationService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: CollectionsDialogParams, ) { @@ -33,6 +35,7 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On cipherService, organizationSerivce, logService, + configService, ); this.cipherId = params?.cipherId; } @@ -47,7 +50,13 @@ export class CollectionsComponent extends BaseCollectionsComponent implements On } check(c: CollectionView, select?: boolean) { - if (!c.canEditItems(this.organization, this.flexibleCollectionsV1Enabled)) { + if ( + !c.canEditItems( + this.organization, + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return; } (c as any).checked = select == null ? !(c as any).checked : select; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html index f4fb2cc040..0b94b6e2be 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.html @@ -1,5 +1,10 @@ - diff --git a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts index dc3a41cf15..90af89e60e 100644 --- a/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-onboarding/vault-onboarding.component.ts @@ -60,9 +60,8 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy { ) {} async ngOnInit() { - this.showOnboardingAccess$ = await this.configService.getFeatureFlag$( + this.showOnboardingAccess$ = await this.configService.getFeatureFlag$( FeatureFlag.VaultOnboarding, - false, ); this.onboardingTasks$ = this.vaultOnboardingService.vaultOnboardingState$; await this.setOnboardingTasks(); diff --git a/apps/web/src/app/vault/individual-vault/vault.component.html b/apps/web/src/app/vault/individual-vault/vault.component.html index 003066dadd..3f95665f37 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.html +++ b/apps/web/src/app/vault/individual-vault/vault.component.html @@ -47,7 +47,6 @@ [showBulkMove]="showBulkMove" [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="false" - [cloneableOrganizationCiphers]="false" [showAdminActions]="false" (onEvent)="onVaultItemsEvent($event)" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled$ | async" diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index a25ba6edbc..b956a90445 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -35,6 +35,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; @@ -147,7 +148,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected currentSearchText$: Observable; protected flexibleCollectionsV1Enabled$ = this.configService.getFeatureFlag$( FeatureFlag.FlexibleCollectionsV1, - false, ); private searchText$ = new Subject(); @@ -184,6 +184,7 @@ export class VaultComponent implements OnInit, OnDestroy { private apiService: ApiService, private userVerificationService: UserVerificationService, private billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -679,6 +680,14 @@ export class VaultComponent implements OnInit, OnDestroy { } else if (result.action === CollectionDialogAction.Deleted) { await this.collectionService.delete(result.collection?.id); this.refresh(); + // Navigate away if we deleted the collection we were viewing + if (this.selectedCollection?.node.id === c?.id) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } @@ -710,9 +719,7 @@ export class VaultComponent implements OnInit, OnDestroy { ); // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.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([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, @@ -966,10 +973,10 @@ export class VaultComponent implements OnInit, OnDestroy { } async isLowKdfIteration() { - const kdfType = await this.stateService.getKdfType(); - const kdfOptions = await this.stateService.getKdfConfig(); + const kdfConfig = await this.kdfConfigService.getKdfConfig(); return ( - kdfType === KdfType.PBKDF2_SHA256 && kdfOptions.iterations < PBKDF2_ITERATIONS.defaultValue + kdfConfig.kdfType === KdfType.PBKDF2_SHA256 && + kdfConfig.iterations < PBKDF2_ITERATIONS.defaultValue ); } diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index c4213989c6..82055cc916 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -81,24 +81,13 @@ export class AddEditComponent extends BaseAddEditComponent { ); } - protected allowOwnershipAssignment() { - if ( - this.ownershipOptions != null && - (this.ownershipOptions.length > 1 || !this.allowPersonal) - ) { - if (this.organization != null) { - return ( - this.cloneMode && this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) - ); - } else { - return !this.editMode || this.cloneMode; - } - } - return false; - } - protected loadCollections() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.loadCollections(); } return Promise.resolve(this.collections); @@ -109,7 +98,10 @@ export class AddEditComponent extends BaseAddEditComponent { const firstCipherCheck = await super.loadCipher(); if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && firstCipherCheck != null ) { return firstCipherCheck; @@ -124,14 +116,24 @@ export class AddEditComponent extends BaseAddEditComponent { } protected encryptCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.encryptCipher(); } return this.cipherService.encrypt(this.cipher, null, null, this.originalCipher); } protected async deleteCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipher(); } return this.cipher.isDeleted diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index cf1f0796ec..30189e8021 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -29,6 +29,7 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On organization: Organization; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccess = false; constructor( cipherService: CipherService, @@ -60,13 +61,19 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On async ngOnInit() { await super.ngOnInit(); this.flexibleCollectionsV1Enabled = await firstValueFrom( - this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false), + this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), + ); + this.restrictProviderAccess = await firstValueFrom( + this.configService.getFeatureFlag$(FeatureFlag.RestrictProviderAccess), ); } protected async reupload(attachment: AttachmentView) { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.showFixOldAttachments(attachment) ) { await super.reuploadCipherAttachment(attachment, true); @@ -74,7 +81,12 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On } protected async loadCipher() { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return await super.loadCipher(); } const response = await this.apiService.getCipherAdmin(this.cipherId); @@ -85,12 +97,20 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On return this.cipherService.saveAttachmentWithServer( this.cipherDomain, file, - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled), + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ), ); } protected deleteCipherAttachment(attachmentId: string) { - if (!this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return super.deleteCipherAttachment(attachmentId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); @@ -99,7 +119,10 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected showFixOldAttachments(attachment: AttachmentView) { return ( attachment.key == null && - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) ); } } diff --git a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts index 091c646178..e13ef49fc3 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collection-assignment-dialog/bulk-collection-assignment-dialog.component.ts @@ -70,13 +70,13 @@ export class BulkCollectionAssignmentDialogComponent implements OnDestroy, OnIni ) {} async ngOnInit() { - const v1FCEnabled = await this.configService.getFeatureFlag( - FeatureFlag.FlexibleCollectionsV1, - false, + const v1FCEnabled = await this.configService.getFeatureFlag(FeatureFlag.FlexibleCollectionsV1); + const restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, ); const org = await this.organizationService.get(this.params.organizationId); - if (org.canEditAllCiphers(v1FCEnabled)) { + if (org.canEditAllCiphers(v1FCEnabled, restrictProviderAccess)) { this.editableItems = this.params.ciphers; } else { this.editableItems = this.params.ciphers.filter((c) => c.edit); diff --git a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts index 337d73b315..7a51f01577 100644 --- a/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts +++ b/apps/web/src/app/vault/org-vault/collection-access-restricted.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from "@angular/core"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; import { ButtonModule, NoItemsModule, svgIcon } from "@bitwarden/components"; @@ -22,12 +22,18 @@ const icon = svgIcon` - {{ "viewCollection" | i18n }} + {{ buttonText | i18n }} `, }) export class CollectionAccessRestrictedComponent { protected icon = icon; + @Input() canEditCollection = false; + @Output() viewCollectionClicked = new EventEmitter(); + + get buttonText() { + return this.canEditCollection ? "editCollection" : "viewCollection"; + } } diff --git a/apps/web/src/app/vault/org-vault/collections.component.ts b/apps/web/src/app/vault/org-vault/collections.component.ts index 67eac2098f..557b048a7b 100644 --- a/apps/web/src/app/vault/org-vault/collections.component.ts +++ b/apps/web/src/app/vault/org-vault/collections.component.ts @@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -35,6 +36,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { organizationService: OrganizationService, private apiService: ApiService, logService: LogService, + configService: ConfigService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: OrgVaultCollectionsDialogParams, ) { @@ -45,6 +47,7 @@ export class CollectionsComponent extends BaseCollectionsComponent { cipherService, organizationService, logService, + configService, dialogRef, params, ); @@ -58,7 +61,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected async loadCipher() { // if cipher is unassigned use apiService. We can see this by looking at this.collectionIds if ( - !this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + !this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) && this.collectionIds.length !== 0 ) { return await super.loadCipher(); @@ -83,7 +89,10 @@ export class CollectionsComponent extends BaseCollectionsComponent { protected saveCollections() { if ( - this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) || + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) || this.collectionIds.length === 0 ) { const request = new CipherCollectionsRequest(this.cipherDomain.collectionIds); diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html index 97d99d5821..8388f4ea9d 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.html @@ -73,8 +73,16 @@ + +
-
+
+ +
diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index c4c67759c7..eecd2f434a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -43,6 +43,9 @@ export class VaultHeaderComponent implements OnInit { /** Currently selected collection */ @Input() collection?: TreeNode; + /** The current search text in the header */ + @Input() searchText: string; + /** Emits an event when the new item button is clicked in the header */ @Output() onAddCipher = new EventEmitter(); @@ -55,10 +58,14 @@ export class VaultHeaderComponent implements OnInit { /** Emits an event when the delete collection button is clicked in the header */ @Output() onDeleteCollection = new EventEmitter(); + /** Emits an event when the search text changes in the header*/ + @Output() searchTextChanged = new EventEmitter(); + protected CollectionDialogTabType = CollectionDialogTabType; protected organizations$ = this.organizationService.organizations$; private flexibleCollectionsV1Enabled = false; + private restrictProviderAccessFlag = false; constructor( private organizationService: OrganizationService, @@ -73,6 +80,9 @@ export class VaultHeaderComponent implements OnInit { this.flexibleCollectionsV1Enabled = await firstValueFrom( this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1), ); + this.restrictProviderAccessFlag = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); } get title() { @@ -80,7 +90,7 @@ export class VaultHeaderComponent implements OnInit { ? this.i18nService.t("collections").toLowerCase() : this.i18nService.t("vault").toLowerCase(); - if (this.collection !== undefined) { + if (this.collection != null) { return this.collection.node.name; } @@ -197,7 +207,23 @@ export class VaultHeaderComponent implements OnInit { return this.collection.node.canDelete(this.organization); } + get canCreateCollection(): boolean { + return this.organization?.canCreateNewCollections; + } + + get canCreateCipher(): boolean { + if (this.organization?.isProviderUser && this.restrictProviderAccessFlag) { + return false; + } + return true; + } + deleteCollection() { this.onDeleteCollection.emit(); } + + onSearchTextChanged(t: string) { + this.searchText = t; + this.searchTextChanged.emit(t); + } } diff --git a/apps/web/src/app/vault/org-vault/vault.component.html b/apps/web/src/app/vault/org-vault/vault.component.html index bcbd56630c..096389021f 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.html +++ b/apps/web/src/app/vault/org-vault/vault.component.html @@ -3,19 +3,20 @@ [loading]="refreshing" [organization]="organization" [collection]="selectedCollection" + [searchText]="currentSearchText$ | async" (onAddCipher)="addCipher()" (onAddCollection)="addCollection()" (onEditCollection)="editCollection(selectedCollection.node, $event.tab)" (onDeleteCollection)="deleteCollection(selectedCollection.node)" + (searchTextChanged)="filterSearchText($event)" >
-
+
-
+
+ + + {{ "all" | i18n }} + + + + {{ "addAccess" | i18n }} + + {{ trashCleanupWarning }} @@ -48,13 +63,14 @@ [showBulkMove]="false" [showBulkTrashOptions]="filter.type === 'trash'" [useEvents]="organization?.useEvents" - [cloneableOrganizationCiphers]="true" [showAdminActions]="true" (onEvent)="onVaultItemsEvent($event)" [showBulkEditCollectionAccess]="organization?.flexibleCollections" [showBulkAddToCollections]="organization?.flexibleCollections" [viewingOrgVault]="true" [flexibleCollectionsV1Enabled]="flexibleCollectionsV1Enabled" + [addAccessStatus]="addAccessStatus$ | async" + [addAccessToggle]="showAddAccessToggle" > @@ -99,8 +115,13 @@ diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 587758dda1..103b29fad7 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -36,6 +36,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; +import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-user/responses"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -97,11 +100,15 @@ import { BulkCollectionsDialogResult, } from "./bulk-collections-dialog"; import { openOrgVaultCollectionsDialog } from "./collections.component"; -import { VaultFilterComponent } from "./vault-filter/vault-filter.component"; const BroadcasterSubscriptionId = "OrgVaultComponent"; const SearchTextDebounceInterval = 200; +enum AddAccessStatusType { + All = 0, + AddAccess = 1, +} + @Component({ selector: "app-org-vault", templateUrl: "vault.component.html", @@ -110,8 +117,6 @@ const SearchTextDebounceInterval = 200; export class VaultComponent implements OnInit, OnDestroy { protected Unassigned = Unassigned; - @ViewChild("vaultFilter", { static: true }) - vaultFilterComponent: VaultFilterComponent; @ViewChild("attachments", { read: ViewContainerRef, static: true }) attachmentsModalRef: ViewContainerRef; @ViewChild("cipherAddEdit", { read: ViewContainerRef, static: true }) @@ -122,6 +127,7 @@ export class VaultComponent implements OnInit, OnDestroy { trashCleanupWarning: string = null; activeFilter: VaultFilter = new VaultFilter(); + protected showAddAccessToggle = false; protected noItemIcon = Icons.Search; protected performingInitialLoad = true; protected refreshing = false; @@ -142,6 +148,10 @@ export class VaultComponent implements OnInit, OnDestroy { protected showMissingCollectionPermissionMessage: boolean; protected showCollectionAccessRestricted: boolean; protected currentSearchText$: Observable; + /** + * A list of collections that the user can assign items to and edit those items within. + * @protected + */ protected editableCollections$: Observable; protected allCollectionsWithoutUnassigned$: Observable; private _flexibleCollectionsV1FlagEnabled: boolean; @@ -149,10 +159,17 @@ export class VaultComponent implements OnInit, OnDestroy { protected get flexibleCollectionsV1Enabled(): boolean { return this._flexibleCollectionsV1FlagEnabled && this.organization?.flexibleCollections; } + protected orgRevokedUsers: OrganizationUserUserDetailsResponse[]; + + private _restrictProviderAccessFlagEnabled: boolean; + protected get restrictProviderAccessEnabled(): boolean { + return this._restrictProviderAccessFlagEnabled && this.flexibleCollectionsV1Enabled; + } private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); + protected addAccessStatus$ = new BehaviorSubject(0); constructor( private route: ActivatedRoute, @@ -181,6 +198,7 @@ export class VaultComponent implements OnInit, OnDestroy { private totpService: TotpService, private apiService: ApiService, private collectionService: CollectionService, + private organizationUserService: OrganizationUserService, protected configService: ConfigService, ) {} @@ -193,7 +211,10 @@ export class VaultComponent implements OnInit, OnDestroy { this._flexibleCollectionsV1FlagEnabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, + ); + + this._restrictProviderAccessFlagEnabled = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, ); const filter$ = this.routedVaultFilterService.filter$; @@ -242,6 +263,11 @@ export class VaultComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.destroy$)) .subscribe((activeFilter) => { this.activeFilter = activeFilter; + + // watch the active filters. Only show toggle when viewing the collections filter + if (!this.activeFilter.collectionId) { + this.showAddAccessToggle = false; + } }); this.searchText$ @@ -281,10 +307,20 @@ export class VaultComponent implements OnInit, OnDestroy { this.editableCollections$ = this.allCollectionsWithoutUnassigned$.pipe( map((collections) => { - // Users that can edit all ciphers can implicitly edit all collections - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + // If restricted, providers can not add items to any collections or edit those items + if (this.organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + // Users that can edit all ciphers can implicitly add to / edit within any collection + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { return collections; } + // The user is only allowed to add/edit items to assigned collections that are not readonly return collections.filter((c) => c.assigned && !c.readOnly); }), shareReplay({ refCount: true, bufferSize: 1 }), @@ -310,12 +346,25 @@ export class VaultComponent implements OnInit, OnDestroy { const allCiphers$ = organization$.pipe( concatMap(async (organization) => { + // If user swaps organization reset the addAccessToggle + if (!this.showAddAccessToggle || organization) { + this.addAccessToggle(0); + } let ciphers; + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return []; + } + if (this.flexibleCollectionsV1Enabled) { // Flexible collections V1 logic. // If the user can edit all ciphers for the organization then fetch them ALL. - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { // Otherwise, only fetch ciphers they have access to (includes unassigned for admins). @@ -323,7 +372,12 @@ export class VaultComponent implements OnInit, OnDestroy { } } else { // Pre-flexible collections logic, to be removed after flexible collections is fully released - if (organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) + ) { ciphers = await this.cipherService.getAllFromApiForOrganization(organization.id); } else { ciphers = (await this.cipherService.getAllDecrypted()).filter( @@ -349,9 +403,21 @@ export class VaultComponent implements OnInit, OnDestroy { shareReplay({ refCount: true, bufferSize: 1 }), ); - const collections$ = combineLatest([nestedCollections$, filter$, this.currentSearchText$]).pipe( + // This will be passed into the usersCanManage call + this.orgRevokedUsers = ( + await this.organizationUserService.getAllUsers(await firstValueFrom(organizationId$)) + ).data.filter((user: OrganizationUserUserDetailsResponse) => { + return user.status === -1; + }); + + const collections$ = combineLatest([ + nestedCollections$, + filter$, + this.currentSearchText$, + this.addAccessStatus$, + ]).pipe( filter(([collections, filter]) => collections != undefined && filter != undefined), - concatMap(async ([collections, filter, searchText]) => { + concatMap(async ([collections, filter, searchText, addAccessStatus]) => { if ( filter.collectionId === Unassigned || (filter.collectionId === undefined && filter.type !== undefined) @@ -359,26 +425,30 @@ export class VaultComponent implements OnInit, OnDestroy { return []; } + this.showAddAccessToggle = false; let collectionsToReturn = []; if (filter.collectionId === undefined || filter.collectionId === All) { - collectionsToReturn = collections.map((c) => c.node); + collectionsToReturn = await this.addAccessCollectionsMap(collections); } else { const selectedCollection = ServiceUtils.getTreeNodeObjectFromList( collections, filter.collectionId, ); - collectionsToReturn = selectedCollection?.children.map((c) => c.node) ?? []; + collectionsToReturn = await this.addAccessCollectionsMap(selectedCollection?.children); } if (await this.searchService.isSearchable(searchText)) { collectionsToReturn = this.searchPipe.transform( collectionsToReturn, searchText, - (collection) => collection.name, - (collection) => collection.id, + (collection: CollectionAdminView) => collection.name, + (collection: CollectionAdminView) => collection.id, ); } + if (addAccessStatus === 1 && this.showAddAccessToggle) { + collectionsToReturn = collectionsToReturn.filter((c: any) => c.addAccess); + } return collectionsToReturn; }), takeUntil(this.destroy$), @@ -407,9 +477,17 @@ export class VaultComponent implements OnInit, OnDestroy { organization$, ]).pipe( map(([filter, collection, organization]) => { + if (organization.isProviderUser && this.restrictProviderAccessEnabled) { + return collection != undefined || filter.collectionId === Unassigned; + } + return ( - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || - (!organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled) && + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || + (!organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ) && collection != undefined && !collection.node.assigned) ); @@ -454,7 +532,8 @@ export class VaultComponent implements OnInit, OnDestroy { map(([filter, collection, organization]) => { return ( // Filtering by unassigned, show message if not admin - (filter.collectionId === Unassigned && !organization.canEditUnassignedCiphers()) || + (filter.collectionId === Unassigned && + !organization.canEditUnassignedCiphers(this.restrictProviderAccessEnabled)) || // Filtering by a collection, so show message if user is not assigned (collection != undefined && !collection.node.assigned && @@ -477,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy { if (this.flexibleCollectionsV1Enabled) { canEditCipher = - organization.canEditAllCiphers(true) || + organization.canEditAllCiphers(true, this.restrictProviderAccessEnabled) || (await firstValueFrom(allCipherMap$))[cipherId] != undefined; } else { canEditCipher = @@ -587,6 +666,60 @@ export class VaultComponent implements OnInit, OnDestroy { ); } + // Update the list of collections to see if any collection is orphaned + // and will receive the addAccess badge / be filterable by the user + async addAccessCollectionsMap(collections: TreeNode[]) { + let mappedCollections; + const { type, allowAdminAccessToAllCollectionItems, permissions } = this.organization; + + const canEditCiphersCheck = + this._flexibleCollectionsV1FlagEnabled && + !this.organization.canEditAllCiphers( + this._flexibleCollectionsV1FlagEnabled, + this.restrictProviderAccessEnabled, + ); + + // This custom type check will show addAccess badge for + // Custom users with canEdit access AND owner/admin manage access setting is OFF + const customUserCheck = + this._flexibleCollectionsV1FlagEnabled && + !allowAdminAccessToAllCollectionItems && + type === OrganizationUserType.Custom && + permissions.editAnyCollection; + + // If Custom user has Delete Only access they will not see Add Access toggle + const customUserOnlyDelete = + this.flexibleCollectionsV1Enabled && + type === OrganizationUserType.Custom && + permissions.deleteAnyCollection && + !permissions.editAnyCollection; + + if (!customUserOnlyDelete && (canEditCiphersCheck || customUserCheck)) { + mappedCollections = collections.map((c: TreeNode) => { + const groupsCanManage = c.node.groupsCanManage(); + const usersCanManage = c.node.usersCanManage(this.orgRevokedUsers); + if ( + groupsCanManage.length === 0 && + usersCanManage.length === 0 && + c.node.id !== Unassigned + ) { + c.node.addAccess = true; + this.showAddAccessToggle = true; + } else { + c.node.addAccess = false; + } + return c.node; + }); + } else { + mappedCollections = collections.map((c: TreeNode) => c.node); + } + return mappedCollections; + } + + addAccessToggle(e: any) { + this.addAccessStatus$.next(e); + } + get loading() { return this.refreshing || this.processingEvent; } @@ -693,13 +826,13 @@ export class VaultComponent implements OnInit, OnDestroy { map((c) => { return c.sort((a, b) => { if ( - a.canEditItems(this.organization, true) && - !b.canEditItems(this.organization, true) + a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + !b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return -1; } else if ( - !a.canEditItems(this.organization, true) && - b.canEditItems(this.organization, true) + !a.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) && + b.canEditItems(this.organization, true, this.restrictProviderAccessEnabled) ) { return 1; } else { @@ -715,33 +848,14 @@ export class VaultComponent implements OnInit, OnDestroy { const dialog = openOrgVaultCollectionsDialog(this.dialogService, { data: { collectionIds: cipher.collectionIds, - collections: collections.filter((c) => !c.readOnly && c.id != Unassigned), + collections: collections, organization: this.organization, cipherId: cipher.id, }, }); - /** - - const [modal] = await this.modalService.openViewRef( - CollectionsComponent, - this.collectionsModalRef, - (comp) => { - comp.flexibleCollectionsV1Enabled = this.flexibleCollectionsV1Enabled; - comp.collectionIds = cipher.collectionIds; - comp.collections = collections; - comp.organization = this.organization; - comp.cipherId = cipher.id; - comp.onSavedCollections.pipe(takeUntil(this.destroy$)).subscribe(() => { - modal.close(); - this.refresh(); - }); - }, - ); - - */ if ((await lastValueFrom(dialog.closed)) == CollectionsDialogResult.Saved) { - await this.refresh(); + this.refresh(); } } @@ -958,11 +1072,9 @@ export class VaultComponent implements OnInit, OnDestroy { this.i18nService.t("deletedCollectionId", collection.name), ); - // Navigate away if we deleted the colletion we were viewing + // Navigate away if we deleted the collection we were viewing if (this.selectedCollection?.node.id === collection.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([], { + void this.router.navigate([], { queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, queryParamsHandling: "merge", replaceUrl: true, @@ -1095,6 +1207,18 @@ export class VaultComponent implements OnInit, OnDestroy { result.action === CollectionDialogAction.Deleted ) { this.refresh(); + + // If we deleted the selected collection, navigate up/away + if ( + result.action === CollectionDialogAction.Deleted && + this.selectedCollection?.node.id === c?.id + ) { + void this.router.navigate([], { + queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null }, + queryParamsHandling: "merge", + replaceUrl: true, + }); + } } } @@ -1169,7 +1293,10 @@ export class VaultComponent implements OnInit, OnDestroy { } protected deleteCipherWithServer(id: string, permanent: boolean) { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccessEnabled, + ); return permanent ? this.cipherService.deleteWithServer(id, asAdmin) : this.cipherService.softDeleteWithServer(id, asAdmin); diff --git a/apps/web/src/app/vault/org-vault/vault.module.ts b/apps/web/src/app/vault/org-vault/vault.module.ts index 47365bb4b1..a478307123 100644 --- a/apps/web/src/app/vault/org-vault/vault.module.ts +++ b/apps/web/src/app/vault/org-vault/vault.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; -import { BreadcrumbsModule, NoItemsModule } from "@bitwarden/components"; +import { BreadcrumbsModule, NoItemsModule, SearchModule } from "@bitwarden/components"; import { LooseComponentsModule } from "../../shared/loose-components.module"; import { SharedModule } from "../../shared/shared.module"; @@ -32,6 +32,7 @@ import { VaultComponent } from "./vault.component"; CollectionDialogModule, CollectionAccessRestrictedComponent, NoItemsModule, + SearchModule, ], declarations: [VaultComponent, VaultHeaderComponent], exports: [VaultComponent], diff --git a/apps/web/src/images/secrets-manager/integrations/ansible.svg b/apps/web/src/images/secrets-manager/integrations/ansible.svg new file mode 100644 index 0000000000..7a32617ab2 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/ansible.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/images/secrets-manager/integrations/github-white.svg b/apps/web/src/images/secrets-manager/integrations/github-white.svg new file mode 100644 index 0000000000..030c7c6723 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github-white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/integrations/github.svg b/apps/web/src/images/secrets-manager/integrations/github.svg new file mode 100644 index 0000000000..99e3ffbbe2 --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg new file mode 100644 index 0000000000..7f7bf006ee --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/images/secrets-manager/integrations/gitlab.svg b/apps/web/src/images/secrets-manager/integrations/gitlab.svg new file mode 100644 index 0000000000..9bbc28c37a --- /dev/null +++ b/apps/web/src/images/secrets-manager/integrations/gitlab.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png b/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png new file mode 100644 index 0000000000..bac17e2ae2 Binary files /dev/null and b/apps/web/src/images/secrets-manager/sdks/c-plus-plus.png differ diff --git a/apps/web/src/images/secrets-manager/sdks/c-sharp.svg b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg new file mode 100644 index 0000000000..f0da2234f1 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/c-sharp.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/go.svg b/apps/web/src/images/secrets-manager/sdks/go.svg new file mode 100644 index 0000000000..d335346e71 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/go.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/java-white.svg b/apps/web/src/images/secrets-manager/sdks/java-white.svg new file mode 100644 index 0000000000..082897c081 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java-white.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/java.svg b/apps/web/src/images/secrets-manager/sdks/java.svg new file mode 100644 index 0000000000..085cedd07c --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/java.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/php.svg b/apps/web/src/images/secrets-manager/sdks/php.svg new file mode 100644 index 0000000000..5e36980ca0 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/php.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/python.svg b/apps/web/src/images/secrets-manager/sdks/python.svg new file mode 100644 index 0000000000..36f5cd7f05 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/python.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/images/secrets-manager/sdks/ruby.png b/apps/web/src/images/secrets-manager/sdks/ruby.png new file mode 100644 index 0000000000..2dac1c8da8 Binary files /dev/null and b/apps/web/src/images/secrets-manager/sdks/ruby.png differ diff --git a/apps/web/src/images/secrets-manager/sdks/wasm.svg b/apps/web/src/images/secrets-manager/sdks/wasm.svg new file mode 100644 index 0000000000..0ed253e242 --- /dev/null +++ b/apps/web/src/images/secrets-manager/sdks/wasm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/src/locales/af/messages.json b/apps/web/src/locales/af/messages.json index 6dfb5ec30d..fd873a41f1 100644 --- a/apps/web/src/locales/af/messages.json +++ b/apps/web/src/locales/af/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Onbeveiligde webwerwe gevind" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Blootgestelde wagwoorde gevind" }, - "exposedPasswordsFoundDesc": { - "message": "Ons het $COUNT$ wagwoorde in u kluis gevind wat in databreuke blootgestel is. U behoort dit te verander en ’n nuwe wagwoord te gebruik.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Swak wagwoorde gevind" }, - "weakPasswordsFoundDesc": { - "message": "Ons het $COUNT$ swak wagwoorde in u kluis gevind. U behoort dit by te werk en sterker wagwoorde te gebruik.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Hergebruikte wagwoorde gevind" }, - "reusedPasswordsFoundDesc": { - "message": "Ons het $COUNT$ wagwoorde in u kluis gewind wat hergebruik word. U behoort dit na ’n unieke waarde te verander.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Verleen toegang tot versamelings deur dit tot hierdie groep toe te voeg." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Verleen toegang tot alle huidige en toekomstige versamelings." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ar/messages.json b/apps/web/src/locales/ar/messages.json index 1923502b1a..bb99982684 100644 --- a/apps/web/src/locales/ar/messages.json +++ b/apps/web/src/locales/ar/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "تم العثور على كلمات مرور معاد استخدامها" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/az/messages.json b/apps/web/src/locales/az/messages.json index 911031f0f1..795b6bee8e 100644 --- a/apps/web/src/locales/az/messages.json +++ b/apps/web/src/locales/az/messages.json @@ -1474,7 +1474,7 @@ "message": "Veb anbarda istifadə olunan dili dəyişdirin." }, "enableFavicon": { - "message": "Veb sayt nişanlarını göstər" + "message": "Veb sayt ikonlarını göstər" }, "faviconDesc": { "message": "Hər girişin yanında tanına bilən təsvir göstər." @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Güvənli olmayan veb sayt tapıldı" }, - "unsecuredWebsitesFoundDesc": { - "message": "Anbarınızda güvənli olmayan URI-lərə sahib $COUNT$ element tapıldı. Veb sayt icazə verirsə, onların URI sxemini https:// olaraq dəyişdirməlisiniz.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$VAULT$ daxilində güvənli olmayan URI-lərə sahib $COUNT$ element tapdıq. Veb sayt icazə verirsə, onların URI sxemini https:// olaraq dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA olmayan hesablar tapıldı" }, - "inactive2faFoundDesc": { - "message": "Anbarınızda (\"2fa.directory\"ə uyğun olaraq) iki addımlı kimlik doğrulama ilə konfiqurasiya edilmədiyini düşündüyümüz $COUNT$ veb sayt tapdıq. Bu hesabları daha çox qorumaq üçün iki addımlı girişi qurmalısınız.", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ daxilində (\"2fa.directory\"ə uyğun olaraq) iki addımlı kimlik doğrulama ilə konfiqurasiya edilmədiyini düşündüyümüz $COUNT$ veb sayt tapdıq. Bu hesabları daha çox qorumaq üçün iki addımlı girişi qurmalısınız.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "İfşa olunmuş parollar tapıldı" }, - "exposedPasswordsFoundDesc": { - "message": "Anbarınızda, bilinən məlumat pozuntusunda parolları ifşa olunmuş $COUNT$ element tapdıq. Yeni bir parol istifadə etməzdən əvvəl onları dəyişdirməlisiniz.", + "exposedPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində, bilinən məlumat pozuntusunda parolları ifşa olunmuş $COUNT$ element tapdıq. Yeni bir parol istifadə etməzdən əvvəl onları dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zəif parol tapıldı" }, - "weakPasswordsFoundDesc": { - "message": "Anbarınızda, şifrələri güclü olmayan $COUNT$ element tapdıq. Güclü şifrələr istifadə etmək üçün onları güncəlləməlisiniz.", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində, parolları güclü olmayan $COUNT$ element tapdıq. Güclü parollar istifadə etmək üçün onları güncəlləməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Təkrar istifadə olunmuş parollar tapıldı" }, - "reusedPasswordsFoundDesc": { - "message": "Anbarınızda təkrar istifadə edilən $COUNT$ parol tapdıq. Onları unikal bir dəyərlə dəyişdirməlisiniz.", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ daxilində təkrar istifadə edilən $COUNT$ parol tapdıq. Onları unikal bir dəyərlə dəyişdirməlisiniz.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -4247,7 +4267,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendProtectedPasswordDontKnow": { - "message": "Şifrəni bilmirsiniz? Bu \"Send\"ə müraciət etmək üçün parolu göndərən şəxsdən istəyin.", + "message": "Parolu bilmirsiniz? Bu \"Send\"ə müraciət etmək üçün parolu göndərən şəxsdən istəyin.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendHiddenByDefault": { @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Bu qrupa əlavə edərək kolleksiyalara müraciət icazəsi verin." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Yalnız idarə etdiyiniz kolleksiyaları təyin edə bilərsiniz." + }, "accessAllCollectionsDesc": { "message": "Hazırkı və gələcəkdəki bütün kolleksiyalara müraciət icazəsi verin." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provayder Portal" }, + "success": { + "message": "Uğurlu" + }, "viewCollection": { "message": "Kolleksiyaya bax" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Maşın hesabına müraciət güncəlləndi" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Özünüzü bir qrupa əlavə edə bilməzsiniz." }, "unassignedItemsBannerSelfHost": { "message": "Bildiriş: 2 May 2024-cü ildən etibarən təyin edilməmiş təşkilat elementləri artıq cihazlar arasında Bütün Anbarlar görünüşündə görünməyən və yalnız Admin Konsolu vasitəsilə əlçatan olacaq. Bu elementləri görünən etmək üçün Admin Konsolundan bir kolleksiyaya təyin edin." + }, + "unassignedItemsBannerNotice": { + "message": "Bildiriş: Təyin edilməyən təşkilat elementləri artıq Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bildiriş: 16 May 2024-cü il tarixindən etibarən, təyin edilməyən təşkilat elementləri cihazlar arasında və Bütün Anbarlar görünüşündə görünən olmayacaq və yalnız Admin Konsolu vasitəsilə əlçatan olacaq." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Bu elementləri görünən etmək üçün", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "bir kolleksiyaya təyin edin.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Provayderi sil" + }, + "deleteProviderConfirmation": { + "message": "Bir provayderin silinməsi daimi və geri qaytarıla bilməyən prosesdir. Provayderin və əlaqəli bütün datanın silinməsini təsdiqləmək üçün ana parolunuzu daxil edin." + }, + "deleteProviderName": { + "message": "$ID$ silinə bilmir", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provayder silindi" + }, + "providerDeletedDesc": { + "message": "Provayder və bütün əlaqəli datalar silindi." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Bu Provayderi silmək üçün tələb göndərdiniz. Təsdiqləmək üçün aşağıdakı düyməni istifadə edin." + }, + "deleteProviderWarning": { + "message": "Provayderin silinməsi daimi prosesdir. Geri dönüşü olmayacaq." + }, + "errorAssigningTargetCollection": { + "message": "Hədəf kolleksiyaya təyin etmə xətası." + }, + "errorAssigningTargetFolder": { + "message": "Hədəf qovluğa təyin etmə xətası." + }, + "integrationsAndSdks": { + "message": "İnteqrasiyalar və SDK-lar", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "İnteqrasiyalar" + }, + "integrationsDesc": { + "message": "\"Bitwarden Sirr Meneceri\"ndəki sirləri avtomatik olaraq üçüncü tərəf xidmətlə sinxronlaşdır." + }, + "sdks": { + "message": "SDK-lar" + }, + "sdksDesc": { + "message": "Öz tətbiqlərinizi qurmaq üçün \"Bitwarden Sirr Meneceri SDK\"sını aşağıdakı proqramlaşdırma dillərində istifadə edin." + }, + "setUpGithubActions": { + "message": "Github Actions qur" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD qur" + }, + "setUpAnsible": { + "message": "Ansible qur" + }, + "cSharpSDKRepo": { + "message": "C# repozitoriyasına bax" + }, + "cPlusPlusSDKRepo": { + "message": "C++ repozitoriyasına bax" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly repozitoriyasına bax" + }, + "javaSDKRepo": { + "message": "Java repozitoriyasına bax" + }, + "pythonSDKRepo": { + "message": "Python repozitoriyasına bax" + }, + "phpSDKRepo": { + "message": "php repozitoriyasına bax" + }, + "rubySDKRepo": { + "message": "Ruby repozitoriyasına bax" + }, + "goSDKRepo": { + "message": "Go repozitoriyasına bax" + }, + "createNewClientToManageAsProvider": { + "message": "Provayder kimi idarə etmək üçün yeni bir client təşkilatı yaradın. Əlavə yerlər növbəti faktura dövründə əks olunacaq." + }, + "selectAPlan": { + "message": "Bir plan seçin" + }, + "thirtyFivePercentDiscount": { + "message": "35% endirim" + }, + "monthPerMember": { + "message": "üzv başına ay" + }, + "seats": { + "message": "Yer" + }, + "addOrganization": { + "message": "Təşkilat əlavə et" + }, + "createdNewClient": { + "message": "Yeni client uğurla yaradıldı" + }, + "noAccess": { + "message": "Müraciət yoxdur" + }, + "collectionAdminConsoleManaged": { + "message": "Bu kolleksiya yalnız admin konsolundan əlçatandır" + }, + "organizationOptionsMenu": { + "message": "Təşkilat Menyusuna keç" + }, + "vaultItemSelect": { + "message": "Anbar elementini seç" + }, + "collectionItemSelect": { + "message": "Kolleksiya elementini seç" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Fakturanı \"Provayder Portalı\"ndan idarə et" } } diff --git a/apps/web/src/locales/be/messages.json b/apps/web/src/locales/be/messages.json index c93f773382..ba424f456b 100644 --- a/apps/web/src/locales/be/messages.json +++ b/apps/web/src/locales/be/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Знойдзены неабароненыя вэб-сайты" }, - "unsecuredWebsitesFoundDesc": { - "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з неабароненымі URI. Вам неабходна змяніць іх схему URI на https://, калі вэб-сайт дазваляе гэта зрабіць.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Знойдзены лагіны без 2ФА" }, - "inactive2faFoundDesc": { - "message": "У сховішчы ёсць вэб-сайты ($COUNT$ шт.), якія могуць быць не наладжаны для двухэтапнай аўтэнтыфікацыі (згодна з 2fa.directory). Для дадатковай абароны гэтых уліковых запісаў уключыце двухэтапную аўтэнтыфікацыю.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Знойдзены скампраметаваныя паролі" }, - "exposedPasswordsFoundDesc": { - "message": "У сховішчы выяўлены элементы ($COUNT$ шт.) з паролямі, якія былі скампраметаваны ў вядомых базах уцечак. Вам неабходна выбраць для іх іншы пароль.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Знойдзены ненадзейныя паролі" }, - "weakPasswordsFoundDesc": { - "message": "У сховішчы ёсць элементы ($COUNT$ шт.) з ненадзейнымі паролямі. Вам неабходна замяніць іх на больш надзейныя.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Знойдзены паўторныя паролі" }, - "reusedPasswordsFoundDesc": { - "message": "У сховішчы ёсць паўторныя паролі ($COUNT$ шт.). Вам неабходна згенерыраваць унікальныя паролі і замяніць іх.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Забяспечыць доступ да калекцый, дадаўшы іх у гэту групу." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Даць доступ да ўсіх бягучых і будучых калекцый." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/bg/messages.json b/apps/web/src/locales/bg/messages.json index b3c20f50e2..5b071f6658 100644 --- a/apps/web/src/locales/bg/messages.json +++ b/apps/web/src/locales/bg/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Открити са записи без защита" }, - "unsecuredWebsitesFoundDesc": { - "message": "Открити са $COUNT$ записи в трезора с адрес без защита. Трябва да смените схемата за URI-то им на „https://“, ако сайтовете го поддържат.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ елемента с незащитени адреси. Препоръчително е да смените протокола им на „https://“, ако уеб сайтовете го поддържат.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Открити са записи без двустепенна идентификация" }, - "inactive2faFoundDesc": { - "message": "В трезора ви има $COUNT$ уебсайт(а), които е възможно да не са настроени за вписване чрез двустепенно удостоверяване (според сайта 2fa.directory). За да защитите тези регистрации, трябва да настроите двустепенното удостоверяване.", + "inactive2faFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ уебсайт(а), които е възможно да не са настроени за вписване чрез двустепенно удостоверяване (според сайта 2fa.directory). За да защитите тези регистрации, трябва да настроите двустепенното удостоверяване.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Открити са разкрити пароли" }, - "exposedPasswordsFoundDesc": { - "message": "Открити са $COUNT$ записа в трезора ви, които са с разкрити пароли. Трябва да смените тези пароли.", + "exposedPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ са открити $COUNT$ елемента с пароли, които са открити в известни случаи на изтичане на данни. Препоръчително е да смените тези пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Открити са слаби пароли" }, - "weakPasswordsFoundDesc": { - "message": "В трезора ви има $COUNT$записи със слаби пароли. Сменете паролите с надеждни.", + "weakPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ елемент(а) със слаби пароли. Препоръчително е да ги смените с по-надеждни.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Отрити са повтарящи се пароли" }, - "reusedPasswordsFoundDesc": { - "message": "В трезора ви има $COUNT$ повтарящи се пароли. Задължително ги сменете с други, уникални пароли.", + "reusedPasswordsFoundReportDesc": { + "message": "Във Вашия $VAULT$ има $COUNT$ повтарящи се пароли. Препоръчително е да ги смените, така че всяка да е уникална.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Дайте достъп до колекциите, като ги добавите към тази група." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Можете да свързвате само колекции, които имате право да управлявате." + }, "accessAllCollectionsDesc": { "message": "Дайте достъп до всички текущи и бъдещи колекции." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Портал за доставчици" }, + "success": { + "message": "Успех" + }, "viewCollection": { "message": "Преглед на колекцията" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Достъпът на машинния акаунт е променен" }, - "unassignedItemsBanner": { - "message": "Известие: неразпределените елементи на организацията вече не се виждат в изгледа с „Всички трезори“ на различните устройства, а са достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + "restrictedGroupAccessDesc": { + "message": "Не може да добавяте себе си към групи." }, "unassignedItemsBannerSelfHost": { "message": "Известие: от 2 май 2024г. неразпределените елементи на организациите вече няма се виждат в изгледа с „Всички трезори“ на различните устройства, а ще бъдат достъпни само през Административната конзола. Добавете тези елементи към някоя колекция в Административната конзола, за да станат видими." + }, + "unassignedItemsBannerNotice": { + "message": "Известие: неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а са достъпни само през Административната конзола." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Известие: след 16 май 2024, неразпределените елементи в организацията вече няма да се виждат в изгледа с всички трезори на различните устройства, а ще бъдат достъпни само през Административната конзола." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Добавете тези елементи към колекция в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "за да ги направите видими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Изтриване на доставчик" + }, + "deleteProviderConfirmation": { + "message": "Изтриването на доставчик е окончателно и необратимо. Въведете главната си парола, за да потвърдите изтриването на доставчика и всички свързани данни." + }, + "deleteProviderName": { + "message": "Изтриването на $ID$ е невъзможно", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Трябва да разкачите всички клиенти, преди да можете да изтриете $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Доставчикът е изтрит" + }, + "providerDeletedDesc": { + "message": "Доставчикът и всички свързани данни са изтрити." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Заявили сте, че искате да изтриете този доставчик. Използвайте бутона по-долу, за да потвърдите това решение." + }, + "deleteProviderWarning": { + "message": "Изтриването на доставчика Ви е окончателно и необратимо." + }, + "errorAssigningTargetCollection": { + "message": "Грешка при задаването на целева колекция." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при задаването на целева папка." + }, + "integrationsAndSdks": { + "message": "Интеграции и набори за разработка (SDK)", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Интеграции" + }, + "integrationsDesc": { + "message": "Автоматично синхронизиране на тайните от „Управлението на тайни“ на Битуорден към външна услуга." + }, + "sdks": { + "message": "Набори за разработка (SDK)" + }, + "sdksDesc": { + "message": "Използвайте набора за разработка (SDK) за Управлението на тайни на Битуорден със следните програмни езици, за да създадете свои собствени приложения." + }, + "setUpGithubActions": { + "message": "Настройка на действия в Github" + }, + "setUpGitlabCICD": { + "message": "Настройка на GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Настройка на Ansible" + }, + "cSharpSDKRepo": { + "message": "Преглед на хранилището за C#" + }, + "cPlusPlusSDKRepo": { + "message": "Преглед на хранилището за C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Преглед на хранилището за JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Преглед на хранилището за Java" + }, + "pythonSDKRepo": { + "message": "Преглед на хранилището за Python" + }, + "phpSDKRepo": { + "message": "Преглед на хранилището за php" + }, + "rubySDKRepo": { + "message": "Преглед на хранилището за Ruby" + }, + "goSDKRepo": { + "message": "Преглед на хранилището за Go" + }, + "createNewClientToManageAsProvider": { + "message": "Създайте нова организация, която да управлявате като доставчик. Допълнителните места ще бъдат отразени в следващия платежен период." + }, + "selectAPlan": { + "message": "Изберете план" + }, + "thirtyFivePercentDiscount": { + "message": "Отстъпка от 35%" + }, + "monthPerMember": { + "message": "на месец за член" + }, + "seats": { + "message": "Места" + }, + "addOrganization": { + "message": "Добавяне на организация" + }, + "createdNewClient": { + "message": "Новият клиент е създаден успешно" + }, + "noAccess": { + "message": "Нямате достъп" + }, + "collectionAdminConsoleManaged": { + "message": "Тази колекция е достъпна само през административната конзола" + }, + "organizationOptionsMenu": { + "message": "Превключване на менюто на организацията" + }, + "vaultItemSelect": { + "message": "Изберете елемент от трезора" + }, + "collectionItemSelect": { + "message": "Изберете елемент от колекцията" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Управление на плащанията от Портала за доставчици" } } diff --git a/apps/web/src/locales/bn/messages.json b/apps/web/src/locales/bn/messages.json index afde1e40d4..0de85f1b9f 100644 --- a/apps/web/src/locales/bn/messages.json +++ b/apps/web/src/locales/bn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/bs/messages.json b/apps/web/src/locales/bs/messages.json index f5cddc79db..de846aa801 100644 --- a/apps/web/src/locales/bs/messages.json +++ b/apps/web/src/locales/bs/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ca/messages.json b/apps/web/src/locales/ca/messages.json index 2bec515116..443348f1e0 100644 --- a/apps/web/src/locales/ca/messages.json +++ b/apps/web/src/locales/ca/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "S'han trobat llocs web no segurs" }, - "unsecuredWebsitesFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta amb URI no segures. Heu de canviar el seu esquema URI a https:// si el lloc web ho permet.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ amb URI no segures. Heu de canviar el seu esquema URI a https:// si el lloc web ho permet.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "S'han trobat registres sense 2FA" }, - "inactive2faFoundDesc": { - "message": "Hem trobat $COUNT$ llocs web a la vostra caixa forta que no es poden configurar amb l'autenticació de dos factors (d'acord amb 2fa.directory). Per protegir encara més aquests comptes, haureu d'habilitar l'autenticació de dos factors.", + "inactive2faFoundReportDesc": { + "message": "Hem trobat $COUNT$ llocs web a $VAULT$ que no es poden configurar amb l'autenticació de dos factors (d'acord amb 2fa.directory). Per protegir encara més aquests comptes, haureu d'habilitar l'autenticació de dos factors.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "S'han trobat contrasenyes exposades" }, - "exposedPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta que tenen contrasenyes que van ser exposades a filtracions de dades conegudes. Heu de canviar-les amb una contrasenya nova.", + "exposedPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ que tenen contrasenyes que van ser exposades a filtracions de dades conegudes. Heu de canviar-les amb una contrasenya nova.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "S'han trobat contrasenyes poc segures" }, - "weakPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ elements a la vostra caixa forta amb contrasenyes que no són fortes. Heu d'actualitzar-les i utilitzar contrasenyes més fortes.", + "weakPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ elements a $VAULT$ amb contrasenyes que no són fortes. Heu d'actualitzar-les i utilitzar contrasenyes més fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "S'han trobat contrasenyes reutilitzades" }, - "reusedPasswordsFoundDesc": { - "message": "Hem trobat $COUNT$ contrasenyes que s'estan reutilitzant a la vostra caixa forta. Heu de canviar-les a un valor únic.", + "reusedPasswordsFoundReportDesc": { + "message": "Hem trobat $COUNT$ contrasenyes que s'estan reutilitzant a $VAULT$. Heu de canviar-les a un valor únic.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Doneu accés a les col·leccions afegint-les a aquest grup." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Només podeu assignar col·leccions que gestioneu." + }, "accessAllCollectionsDesc": { "message": "Doneu accés a totes les col·leccions actuals i futures." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portal del proveïdor" }, + "success": { + "message": "Èxit" + }, "viewCollection": { "message": "Mostra col·lecció" }, @@ -7625,19 +7651,19 @@ "message": "Assigna a aquestes col·leccions" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Seleccioneu les col·leccions amb les quals es compartiran els elements. Una vegada que un element s'actualitza en una col·lecció, es reflectirà a totes les col·leccions. Només els membres de l'organització amb accés a aquestes col·leccions podran veure els elements." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Seleccioneu les col·leccions per assignar" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "No s'ha assignat cap col·lecció" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Col·leccions assignades correctament" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Heu seleccionat $TOTAL_COUNT$ elements. No podeu actualitzar-ne $READONLY_COUNT$ dels quals perquè no teniu permisos d'edició.", "placeholders": { "total_count": { "content": "$1", @@ -7650,55 +7676,55 @@ } }, "items": { - "message": "Items" + "message": "Elements" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Seients assignats" }, "assigned": { - "message": "Assigned" + "message": "Assignat" }, "used": { - "message": "Used" + "message": "Utilitzat" }, "remaining": { - "message": "Remaining" + "message": "Queden" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desenllaça l'organització" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GESTIONA SEIENTS" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Els ajustos dels seients es reflectiran en el pròxim cicle de facturació." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Seients de subscripció no assignats" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Seients addicionals adquirits" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Els seients assignats no es poden actualitzar. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "L'actualització de la subscripció ha fallat" }, "trial": { - "message": "Trial", + "message": "Prova", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Vençuda", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "La subscripció ha caducat", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data de caducitat de la vostra subscripció per mantenir-la. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7738,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Teniu un període de gràcia de $DAYS$ dies a partir de la data en què cal que la primera factura no pagada mantinga la subscripció. Resoleu les factures vençudes abans de $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7726,54 +7752,54 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Factura no pagada", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Per reactivar la subscripció, resoleu les factures vençudes.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancel·lació", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "No es poden crear comptes de màquina en organitzacions suspeses. Poseu-vos en contacte amb el propietari de l'organització per obtenir ajuda." }, "machineAccount": { - "message": "Machine account", + "message": "Compte de màquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Compte de màquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Compte nou de màquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crea un compte de màquina nou per començar a automatitzar l'accés secret.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Encara no hi ha res a mostrar", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Suprimeix els comptes de màquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Suprimeix comptes de màquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Veure el compte de màquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "La supressió del compte de màquina $MACHINE_ACCOUNT$ és permanent i irreversible.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7808,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "La supressió de comptes de màquina és permanent i irreversible." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Suprimeix $COUNT$ comptes de màquina", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7820,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "S'ha suprimit el compte de màquina" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "S'han suprimit els comptes de màquina" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Cerca els comptes de màquina", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Edita el compte de màquina", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nom del compte de màquina", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "S'ha creat el compte de màquina", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "S'ha actualitzat el compte de màquina", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Dona accés a aquest projecte als comptes de màquina." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Escriu o selecciona comptes de màquina" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Afig comptes de màquina per concedir accés" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Concediu accés a aquest compte de màquina a grups o persones." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Assigna projectes a aquest compte de màquina. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Crea un compte de màquina" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "La supressió de persones d'un compte de màquina no suprimeix els testimonis d'accés que han creat. Per a les millors pràctiques de seguretat, es recomana revocar els testimonis d'accés creats per persones suprimides d'un compte de màquina." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Suprimeix l'accés a aquest compte de màquina" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Aquesta acció suprimirà l'accés al compte de màquina." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ comptes de màquina inclosos", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7882,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ al mes per a comptes de màquina addicionals", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7891,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Comptes de màquina addicionals" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "El vostre pla inclou $COUNT$ comptes de màquina.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7903,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Podeu afegir comptes de màquina addicionals per $COST$ al mes.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7912,168 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limita els comptes de màquina (opcional)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Estableix un límit pels comptes de màquina. Una vegada assolit aquest límit, no en podreu crear de nous." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Límit del compte de màquina (opcional)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Cost potencial màxim del compte de màquina" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Accés al compte de màquina actualitzat" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "No podeu afegir-vos vosaltres mateix a un grup." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Avís: el 2 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la vista \"Totes les caixes fortes\" en tots els dispositius i només es podran accedir des de la Consola d'administració. Assigna aquests elements a una col·lecció des de la Consola d'administració per fer-los visibles." + }, + "unassignedItemsBannerNotice": { + "message": "Avís: els elements de l'organització no assignats ja no són visibles a la visualització de Totes les caixes fortes en tots els dispositius i ara només es poden accedir des de la Consola d'administració." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Avís: el 16 de maig de 2024, els elements de l'organització no assignats deixaran de ser visibles a la visualització de Totes les caixes fortes en tots els dispositius i només es podran accedir des de la Consola d'administració." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assigna aquests elements a una col·lecció de", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per fer-los visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Suprimeix proveïdor" + }, + "deleteProviderConfirmation": { + "message": "La supressió d'un proveïdor és permanent i irreversible. Introduïu la contrasenya mestra per confirmar la supressió del proveïdor i totes les dades associades." + }, + "deleteProviderName": { + "message": "No es pot suprimir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "S'ha suprimit el proveïdor" + }, + "providerDeletedDesc": { + "message": "S'han suprimit el proveïdor i totes les dades associades." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Heu sol·licitat suprimir aquest proveïdor. Utilitzeu el botó següent per confirmar." + }, + "deleteProviderWarning": { + "message": "La supressió del proveïdor és permanent. No es pot desfer." + }, + "errorAssigningTargetCollection": { + "message": "S'ha produït un error en assignar la col·lecció de destinació." + }, + "errorAssigningTargetFolder": { + "message": "S'ha produït un error en assignar la carpeta de destinació." + }, + "integrationsAndSdks": { + "message": "Integracions i SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integracions" + }, + "integrationsDesc": { + "message": "Sincronitza automàticament els secrets de Bitwarden Gestor de secrets amb un servei de tercers." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Utilitzeu Bitwarden gestor de secrets SDK en els següents llenguatges de programació per crear les vostres aplicacions." + }, + "setUpGithubActions": { + "message": "Configura les accions de Github" + }, + "setUpGitlabCICD": { + "message": "Configura GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configura Ansible" + }, + "cSharpSDKRepo": { + "message": "Veure el repositori C#" + }, + "cPlusPlusSDKRepo": { + "message": "Veure el repositori C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Veure el repositori JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Veure el repositori de Java" + }, + "pythonSDKRepo": { + "message": "Veure el repositori de Python" + }, + "phpSDKRepo": { + "message": "Veure el repositori php" + }, + "rubySDKRepo": { + "message": "Veure el repositori Ruby" + }, + "goSDKRepo": { + "message": "Veure el repositori Go" + }, + "createNewClientToManageAsProvider": { + "message": "Creeu una nova organització client per gestionar com a proveïdor. Els seients addicionals es reflectiran en el proper cicle de facturació." + }, + "selectAPlan": { + "message": "Seleccioneu un pla" + }, + "thirtyFivePercentDiscount": { + "message": "35% de descompte" + }, + "monthPerMember": { + "message": "mes per membre" + }, + "seats": { + "message": "Seients" + }, + "addOrganization": { + "message": "Afig organització" + }, + "createdNewClient": { + "message": "S'ha creat un nou client correctament" + }, + "noAccess": { + "message": "Sense accés" + }, + "collectionAdminConsoleManaged": { + "message": "Aquesta col·lecció només és accessible des de la consola d'administració" + }, + "organizationOptionsMenu": { + "message": "Canvia el menú d'organització" + }, + "vaultItemSelect": { + "message": "Seleccioneu element de la caixa forta" + }, + "collectionItemSelect": { + "message": "Seleccioneu element de la col·lecció" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gestioneu la facturació des del portal de proveïdors" } } diff --git a/apps/web/src/locales/cs/messages.json b/apps/web/src/locales/cs/messages.json index 82dfe2ecae..ae9df7bb1d 100644 --- a/apps/web/src/locales/cs/messages.json +++ b/apps/web/src/locales/cs/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Nalezena nezabezpečená webová stránka" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru, které používají nezabezpečené URI. Schémata URI by měla být změněna na https://, pokud to web umožňuje.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru $VAULT$, které používají nezabezpečené URI. Schémata URI by měla být změněna na https://, pokud to web umožňuje.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Nalezena přihlášení bez dvoufázového ověření" }, - "inactive2faFoundDesc": { - "message": "Nalezli jsme $COUNT$ webových stránek ve Vašem trezoru, které zřejmě nejsou nakonfigurovány pro použití dvoufaktorového přihlášení. Pro lepší ochranu Vašich účtů byste měli dvoufaktorové přihlášení povolit.", + "inactive2faFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ webových stránek ve Vašem trezoru $VAULT$, které zřejmě nejsou nakonfigurovány pro použití dvoufaktorového přihlášení. Pro lepší ochranu Vašich účtů byste měli dvoufaktorové přihlášení povolit.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Nalezena odhalená hesla" }, - "exposedPasswordsFoundDesc": { - "message": "Nalezli jsme $COUNT$ položek ve Vašem trezoru, jejichž hesla byla odhalena během známých úniků dat. Měli byste u nich použít nové heslo.", + "exposedPasswordsFoundReportDesc": { + "message": "Našli jsme $COUNT$ položek ve Vašem trezoru $VAULT$, která mají hesla, které byly odhaleny ve známých únicích dat. Měli byste je změnit tak, aby používaly nové heslo.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Nalezena slabá hesla" }, - "weakPasswordsFoundDesc": { - "message": "Našli jsme $COUNT$ položek ve Vašem trezoru se slabým heslem. Měli byste je aktualizovat a použit silnější hesla.", + "weakPasswordsFoundReportDesc": { + "message": "Našli jsme $COUNT$ položek ve Vašem trezoru $VAULT$ se slabým heslem. Měli byste je aktualizovat a použit silnější hesla.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Nalezena opakovaně použitá hesla" }, - "reusedPasswordsFoundDesc": { - "message": "Nalezli jsme $COUNT$ opakovaně použitých hesel ve Vašem trezoru. Doporučujeme je změnit, aby byly unikátní.", + "reusedPasswordsFoundReportDesc": { + "message": "Nalezli jsme $COUNT$ opakovaně použitých hesel ve Vašem trezoru $VAULT$. Doporučujeme je změnit, aby byly unikátní.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Udělí členům přístup ke kolekcím přidáním do této skupiny." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Můžete přiřadit jen Vámi spravované kolekce." + }, "accessAllCollectionsDesc": { "message": "Udělí přístup ke všem aktuálním i budoucím kolekcím." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portál poskytovatele" }, + "success": { + "message": "Úspěch" + }, "viewCollection": { "message": "Zobrazit kolekci" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Přístup strojového účtu byl aktualizován" }, - "unassignedItemsBanner": { - "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné jen v konzoli správce. Přiřaďte tyto položky do kolekce z konzole pro správce, aby byly viditelné." + "restrictedGroupAccessDesc": { + "message": "Do skupiny nemůžete přidat sami sebe." }, "unassignedItemsBannerSelfHost": { "message": "Upozornění: Dne 2. května 2024 již nebudou nepřiřazené položky organizace viditelné v zobrazení Všechny trezory ve všech zařízeních a budou přístupné jen prostřednictvím konzoly správce. Přiřaďte tyto položky do kolekce z konzoly pro správce, aby byly viditelné." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornění: Nepřiřazené položky organizace již nejsou viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a jsou nyní přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornění: 16. květba 2024 již nebudou nepřiřazené položky organizace viditelné ve Vašem zobrazení všech trezorů napříč zařízeními a budou přístupné pouze v konzoli správce." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Přiřadit tyto položky ke kolekci z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "aby byly viditelné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Smazat poskytovatele" + }, + "deleteProviderConfirmation": { + "message": "Smazání poskytovatele je trvalé a nevratné. Zadejte hlavní heslo pro potvrzení smazání poskytovatele a všech souvisejících dat." + }, + "deleteProviderName": { + "message": "Nelze smazat $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Před smazáním $ID$ musíte odpojit všechny klienty.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Poskytovatel byl smazán" + }, + "providerDeletedDesc": { + "message": "Poskytovatel a veškerá související data byla smazána." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Požádali jste o smazání tohoto poskytovatele. Pro potvrzení použijte tlačítko níže." + }, + "deleteProviderWarning": { + "message": "Smazání poskytovatele je trvalé. Tuto akci nelze vrátit zpět." + }, + "errorAssigningTargetCollection": { + "message": "Chyba při přiřazování cílové kolekce." + }, + "errorAssigningTargetFolder": { + "message": "Chyba při přiřazování cílové složky." + }, + "integrationsAndSdks": { + "message": "Integrace a SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrace" + }, + "integrationsDesc": { + "message": "Automaticky synchronizuje tajné klíče se správce tajných klíčů Bitwardenu do služby třetí strany." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Použije SDK správce tajných klíčů Bitwardenu v následujících programovacích jazycích k vytvoření vlastních aplikací." + }, + "setUpGithubActions": { + "message": "Nastavit akce GitHubu" + }, + "setUpGitlabCICD": { + "message": "Nastavit GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Nastavit Ansible" + }, + "cSharpSDKRepo": { + "message": "Zobrazit repozitář C#" + }, + "cPlusPlusSDKRepo": { + "message": "Zobrazit repozitář C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Zobrazit repozitář JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Zobrazit repozitář Java" + }, + "pythonSDKRepo": { + "message": "Zobrazit repozitář Python" + }, + "phpSDKRepo": { + "message": "Zobrazit repozitář PHP" + }, + "rubySDKRepo": { + "message": "Zobrazit repozitář Ruby" + }, + "goSDKRepo": { + "message": "Zobrazit repozitář Go" + }, + "createNewClientToManageAsProvider": { + "message": "Vytvořte novou klientskou organizaci pro správu jako poskytovatele. Další uživatelé budou reflektováni v dalším platebním cyklu." + }, + "selectAPlan": { + "message": "Vyberte plán" + }, + "thirtyFivePercentDiscount": { + "message": "35% sleva" + }, + "monthPerMember": { + "message": "měsíčně za člena" + }, + "seats": { + "message": "Počet" + }, + "addOrganization": { + "message": "Přidat organizaci" + }, + "createdNewClient": { + "message": "Nový klient byl úspěšně vytvořen" + }, + "noAccess": { + "message": "Žádný přístup" + }, + "collectionAdminConsoleManaged": { + "message": "Tato kolekce je přístupná pouze z konzole správce" + }, + "organizationOptionsMenu": { + "message": "Přepnout menu organizace" + }, + "vaultItemSelect": { + "message": "Vybrat položku trezoru" + }, + "collectionItemSelect": { + "message": "Vybrat položku kolekce" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Spravovat fakturaci z portálu poskytovatele" } } diff --git a/apps/web/src/locales/cy/messages.json b/apps/web/src/locales/cy/messages.json index 8ee7ab3569..791f940f11 100644 --- a/apps/web/src/locales/cy/messages.json +++ b/apps/web/src/locales/cy/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/da/messages.json b/apps/web/src/locales/da/messages.json index e52510ed1c..f4b6559cfc 100644 --- a/apps/web/src/locales/da/messages.json +++ b/apps/web/src/locales/da/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Ikke-sikrede websteder fundet" }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med ikke-sikrede URI'er. Deres URI-protokoller bør ændres til https://, såfremt webstedet tillader det.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$COUNT$ emner fundet i $VAULT$ med ikke-sikreded URI'er. Hvis tilladt af webstedet, bør deres URI-protokol ændres til https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins uden totrins-login fundet" }, - "inactive2faFoundDesc": { - "message": "$COUNT$ websted(er) fundet i boksen, som muligvis ikke er opsat med totrins-login (jf. 2fa.directory). For yderligere at beskytte disse konti, bør totrins-login opsættes.", + "inactive2faFoundReportDesc": { + "message": "$COUNT$ websted(er) muligvis uden opsat med tofaktorgodkendelse (iflg. 2fa.directory) fundet i $VAULT$. For yderligere beskyttelse af disse konti bør tofaktorgodkendelse opsættes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kompromitterede adgangskoder fundet" }, - "exposedPasswordsFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med adgangskoder kompromitteret i kendte datalæk. En ny adgangskode bør opsættes for disse emner.", + "exposedPasswordsFoundReportDesc": { + "message": "$COUNT$ emner fundet i $VAULT$ med adgangskoder kompromitteret via kendte datalæk. Deres adgangskoder bør skiftes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svage adgangskoder fundet" }, - "weakPasswordsFoundDesc": { - "message": "$COUNT$ emner fundet i din boks med adgangskoder, som ikke er stærke. Disse bør opdateres med stærkere adgangskoder.", + "weakPasswordsFoundReportDesc": { + "message": "$COUNT$ emner med svage adgangskoder fundet i $VAULT$. Disse bør skiftes til stærke adgangskoder.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Genbrugte adgangskoder fundet" }, - "reusedPasswordsFoundDesc": { - "message": "Vi fandt $COUNT$ adgangskoder, der genbruges i din boks. Du bør ændre dem til unikke koder.", + "reusedPasswordsFoundReportDesc": { + "message": "$COUNT$ adgangskoder, som genbruges, fundet i $VAULT$. Disse bør skiftes til unikke adgangskoder.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Tildel adgang til samlinger ved at føje dem til denne gruppe." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Der kan kun tildeles samlinger, man selv håndterer." + }, "accessAllCollectionsDesc": { "message": "Tildel adgang til alle nuværende og fremtidige samlinger." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Udbyderportal" }, + "success": { + "message": "Gennemført" + }, "viewCollection": { "message": "Vis samling" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Maskinekontoadgang opdateret" }, - "unassignedItemsBanner": { - "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen og er kun tilgængelige via Adminkonsollen. Føj disse emner til en samling fra Adminkonsollen for at gøre dem synlige." + "restrictedGroupAccessDesc": { + "message": "Man kan ikke føje sig selv til en gruppe." }, "unassignedItemsBannerSelfHost": { "message": "Bemærk: Pr. 2. maj 2024 vil utildelte organisationsemner ikke længere være synlige i Alle Bokse-visningen på tværs af enheder og vil kun være tilgængelige via Admin-konsollen. Tildel disse emner til en samling via Admin-konsollen for at gøre dem synlige." + }, + "unassignedItemsBannerNotice": { + "message": "Bemærk: Utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Bemærk: Pr. 16. maj 2024 er utildelte organisationsemner er ikke længere synlige i Alle Bokse-visningen, men er kun tilgængelige via Admin-konsol." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Tildel disse emner til en samling via", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "for at gøre dem synlige.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Slet udbyder" + }, + "deleteProviderConfirmation": { + "message": "Sletning af en udbyder er permanent og irreversibel. Angiv hovedadgangskoden for at bekræfte sletningen af udbyderen og alle tilknyttede data." + }, + "deleteProviderName": { + "message": "Kan ikke slette $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Alle klienttilknytninger skal fjernes, før $ID$ kan slettes.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Udbyder er hermed slettet" + }, + "providerDeletedDesc": { + "message": "Udbyderen og alle tilknyttede data er hermed slettet." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Der er anmodet sletning af din Bitwarden-konto. Klik på knappen nedenfor for at bekræfte." + }, + "deleteProviderWarning": { + "message": "Sletning af kontoen er permanent og irreversibel." + }, + "errorAssigningTargetCollection": { + "message": "Fejl ved tildeling af målsamling." + }, + "errorAssigningTargetFolder": { + "message": "Fejl ved tildeling af målmappe." + }, + "integrationsAndSdks": { + "message": "Integrationer og SDK'er", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrationer" + }, + "integrationsDesc": { + "message": "Synk automatisk hemmeligheder fra Bitwarden Secrets Manager til en tredjepartstjeneste." + }, + "sdks": { + "message": "SDK'er" + }, + "sdksDesc": { + "message": "Brug Bitwarden Secrets Manager SDK i flg. programmeringssprog til bygning af egne applikationer." + }, + "setUpGithubActions": { + "message": "Opsæt Github-handlinger" + }, + "setUpGitlabCICD": { + "message": "Opsæt GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Opsæt Ansible" + }, + "cSharpSDKRepo": { + "message": "Vis C#-repo" + }, + "cPlusPlusSDKRepo": { + "message": "Vis C++-repo" + }, + "jsWebAssemblySDKRepo": { + "message": "VIs JS WebAssembly-repo" + }, + "javaSDKRepo": { + "message": "Vis Java-repo" + }, + "pythonSDKRepo": { + "message": "Vis Python-repo" + }, + "phpSDKRepo": { + "message": "Vis php-repo" + }, + "rubySDKRepo": { + "message": "Vis Ruby-repo" + }, + "goSDKRepo": { + "message": "Vis Go-repo" + }, + "createNewClientToManageAsProvider": { + "message": "Opret en ny kundeorganisation til at håndtere som udbyder. Yderligere pladser afspejles i næste faktureringscyklus." + }, + "selectAPlan": { + "message": "Vælg en abonnementstype" + }, + "thirtyFivePercentDiscount": { + "message": "35% rabat" + }, + "monthPerMember": { + "message": "måned pr. medlem" + }, + "seats": { + "message": "Pladser" + }, + "addOrganization": { + "message": "Tilføj organisation" + }, + "createdNewClient": { + "message": "Ny kunde er hermed oprettet" + }, + "noAccess": { + "message": "Ingen adgang" + }, + "collectionAdminConsoleManaged": { + "message": "Denne samling er kun tilgængelig via Admin-konsol" + }, + "organizationOptionsMenu": { + "message": "Skift Organisationsmenu" + }, + "vaultItemSelect": { + "message": "Vælg boksemne" + }, + "collectionItemSelect": { + "message": "Vælg samlingsemne" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Håndter fakturering via udbyderportalen" } } diff --git a/apps/web/src/locales/de/messages.json b/apps/web/src/locales/de/messages.json index 0e9b0a983f..f7d0e08273 100644 --- a/apps/web/src/locales/de/messages.json +++ b/apps/web/src/locales/de/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Ungesicherte Websites gefunden" }, - "unsecuredWebsitesFoundDesc": { - "message": "Wir haben $COUNT$ Einträge in deinem Tresor mit ungesicherten URIs gefunden. Du solltest ihr URI-Präfix auf https:// ändern, wenn die Website dies zulässt.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ mit ungeschützen URIs gefunden. Du solltest deren URI-Präfix in https:// ändern, wenn die Webseite dies zulässt.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Zugangsdaten ohne 2FA gefunden" }, - "inactive2faFoundDesc": { - "message": "Wir haben $COUNT$ Webseite(n) in Ihrem Tresor gefunden, die eine Zwei-Faktor Authentifizierung anbieten (laut 2fa.directory), aber bei denen diese Funktion möglicherweise nicht aktiviert ist. Um diese Accounts abzusichern, sollten Sie die Zwei-Faktor Authentifizierung aktivieren.", + "inactive2faFoundReportDesc": { + "message": "Wir haben $COUNT$ Website(s) in deinem/deinen $VAULT$ gefunden, die eine Zwei-Faktor-Authentifizierung anbieten (laut 2fa.directory), aber bei denen diese Funktion möglicherweise nicht aktiviert ist. Um diese Konten weiter abzusichern, solltest du die Zwei-Faktor-Authentifizierung aktivieren.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Es wurden kompromittierte Passwörter gefunden" }, - "exposedPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Einträge in deinem Tresor gefunden, die in bekannten Passwortdiebstählen veröffentlicht wurden. Du solltest diese Passwörter so schnell wie möglich ändern.", + "exposedPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ gefunden, die Passwörter enthalten, die in bekannten Passwortdiebstahl-Datenbanken veröffentlicht wurden. Du solltest diese ändern und ein neues Passwort verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Schwache Passwörter gefunden" }, - "weakPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Einträge mit schwachen Passwörtern in deinem Tresor gefunden. Du solltest diese aktualisieren und ein sicheres Passwort verwenden.", + "weakPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Einträge in deinem/deinen $VAULT$ mit unsicheren Passwörtern gefunden. Du solltest diese ändern und sicherere Passwörter verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Wiederverwendete Passwörter gefunden" }, - "reusedPasswordsFoundDesc": { - "message": "Wir haben $COUNT$ Passwörter in deinem Tresor gefunden, die mehrfach benutzt wurden. Du solltest diese ändern und jedes Passwort nur ein einziges Mal benutzen.", + "reusedPasswordsFoundReportDesc": { + "message": "Wir haben $COUNT$ Passwörter in deinem/deinen $VAULT$ gefunden, die mehrfach verwendet werden. Du solltest diese ändern und einzigartige Passwörter verwenden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -4951,7 +4971,7 @@ "message": "Erstelle eine neue Kunden-Organisation, die dir als Anbieter zugeordnet wird. Du kannst auf diese Organisation zugreifen und diese verwalten." }, "newClient": { - "message": "New client" + "message": "Neuer Kunde" }, "addExistingOrganization": { "message": "Bestehende Organisation hinzufügen" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Gewähre Zugriff auf Sammlungen, indem du diese zu dieser Gruppe hinzufügst." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Du kannst nur von dir verwaltete Sammlungen zuweisen." + }, "accessAllCollectionsDesc": { "message": "Gewähre Zugriff auf alle aktuellen und zukünftigen Sammlungen." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Anbieterportal" }, + "success": { + "message": "Erfolg" + }, "viewCollection": { "message": "Sammlung anzeigen" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Zugriff auf Gerätekonto aktualisiert" }, - "unassignedItemsBanner": { - "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und sind nun nur über die Administrator-Konsole zugänglich. Weise diese Einträge einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + "restrictedGroupAccessDesc": { + "message": "Du kannst dich nicht selbst zu einer Gruppe hinzufügen." }, "unassignedItemsBannerSelfHost": { "message": "Hinweis: Ab dem 2. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und sind nur über die Administrator-Konsole zugänglich. Weise diese Elemente einer Sammlung aus der Administrator-Konsole zu, um sie sichtbar zu machen." + }, + "unassignedItemsBannerNotice": { + "message": "Hinweis: Nicht zugewiesene Organisationseinträge sind nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar und nun nur über die Administrator-Konsole zugänglich." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Hinweis: Ab dem 16. Mai 2024 werden nicht zugewiesene Organisationseinträge nicht mehr geräteübergreifend in der Ansicht aller Tresore sichtbar sein und nur über die Administrator-Konsole zugänglich." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Weise diese Einträge einer Sammlung aus der", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "zu, um sie sichtbar zu machen.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Anbieter löschen" + }, + "deleteProviderConfirmation": { + "message": "Das Löschen eines Anbieters ist dauerhaft und unwiderruflich. Gib dein Master-Passwort ein, um die Löschung des Anbieters und aller zugehörigen Daten zu bestätigen." + }, + "deleteProviderName": { + "message": "Kann $ID$ nicht löschen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Anbieter gelöscht" + }, + "providerDeletedDesc": { + "message": "Der Anbieter und alle zugehörigen Daten wurden gelöscht." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Du hast die Löschung dieses Anbieters angefragt. Verwende den Button unten, um dies zu bestätigen." + }, + "deleteProviderWarning": { + "message": "Die Löschung deines Anbieters ist dauerhaft. Sie kann nicht widerrufen werden." + }, + "errorAssigningTargetCollection": { + "message": "Fehler beim Zuweisen der Ziel-Sammlung." + }, + "errorAssigningTargetFolder": { + "message": "Fehler beim Zuweisen des Ziel-Ordners." + }, + "integrationsAndSdks": { + "message": "Integrationen & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrationen" + }, + "integrationsDesc": { + "message": "Geheimnisse des Bitwarden Secrets Managers automatisch mit einem Drittanbieter-Dienst synchronisieren." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Verwende das Bitwarden Secrets Manager SDK in den folgenden Programmiersprachen, um deine eigenen Anwendungen zu erstellen." + }, + "setUpGithubActions": { + "message": "GitHub Actions einrichten" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD einrichten" + }, + "setUpAnsible": { + "message": "Ansible einrichten" + }, + "cSharpSDKRepo": { + "message": "C#-Repository anzeigen" + }, + "cPlusPlusSDKRepo": { + "message": "C++-Repository anzeigen" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly-Repository anzeigen" + }, + "javaSDKRepo": { + "message": "Java-Repository anzeigen" + }, + "pythonSDKRepo": { + "message": "Python-Repository anzeigen" + }, + "phpSDKRepo": { + "message": "PHP-Repository anzeigen" + }, + "rubySDKRepo": { + "message": "Ruby-Repository anzeigen" + }, + "goSDKRepo": { + "message": "Go-Repository anzeigen" + }, + "createNewClientToManageAsProvider": { + "message": "Erstelle eine neue Kunden-Organisation, um sie als Anbieter zu verwalten. Zusätzliche Benutzerplätze werden im nächsten Abrechnungszeitraum berücksichtigt." + }, + "selectAPlan": { + "message": "Ein Abo auswählen" + }, + "thirtyFivePercentDiscount": { + "message": "35% Rabatt" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Benutzerplätze" + }, + "addOrganization": { + "message": "Organisation hinzufügen" + }, + "createdNewClient": { + "message": "Neuer Kunde erfolgreich erstellt" + }, + "noAccess": { + "message": "Kein Zugriff" + }, + "collectionAdminConsoleManaged": { + "message": "Diese Sammlung ist nur über die Administrator-Konsole zugänglich" + }, + "organizationOptionsMenu": { + "message": "Organisationsmenü umschalten" + }, + "vaultItemSelect": { + "message": "Tresor-Eintrag auswählen" + }, + "collectionItemSelect": { + "message": "Sammlungseintrag auswählen" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Rechnungen über das Anbieter-Portal verwalten" } } diff --git a/apps/web/src/locales/el/messages.json b/apps/web/src/locales/el/messages.json index 256a5644d6..6dab7f4816 100644 --- a/apps/web/src/locales/el/messages.json +++ b/apps/web/src/locales/el/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Βρέθηκαν μη ασφαλής ιστοσελίδες" }, - "unsecuredWebsitesFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, με μη ασφαλές URI. Θα πρέπει να αλλάξετε το URI σε https:/ εφόσον το επιτρέπει η ιστοσελίδα.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Βρέθηκαν Συνδέσεις Χωρίς 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Βρέθηκαν Εκτεθειμένοι Κωδικοί" }, - "exposedPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, που έχουν εκτεθειμένους κωδικούς σε γνωστές παραβιάσεις δεδομένων. Θα πρέπει να τους αλλάξετε προκειμένου να χρησιμοποιήσετε έναν νέο κωδικό.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Βρέθηκαν Αδύναμοι Κωδικοί" }, - "weakPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ στοιχεία στο vault σας, με κωδικούς που δεν είναι ισχυροί. Θα πρέπει να τους ενημερώσετε και να χρησιμοποιήσετε ισχυρότερους κωδικούς.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Βρέθηκαν Επανα-χρησιμοποιούμενοι Κωδικοί" }, - "reusedPasswordsFoundDesc": { - "message": "Βρήκαμε $COUNT$ κωδικούς που επανα-χρησιμοποιούνται στο vault σας. Θα πρέπει να αλλάξετε τον κάθε ένα, σε κάτι μοναδικό.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 06127326c4..d80386d0cc 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1812,12 +1812,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1833,12 +1837,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1857,12 +1865,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1890,12 +1902,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1911,12 +1927,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2771,6 +2791,12 @@ "all": { "message": "All" }, + "addAccess": { + "message": "Add Access" + }, + "addAccessFilter": { + "message": "Add Access Filter" + }, "refresh": { "message": "Refresh" }, @@ -6466,6 +6492,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7612,6 +7641,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7909,6 +7941,9 @@ "restrictedGroupAccessDesc": { "message": "You cannot add yourself to a group." }, + "unassignedItemsBannerSelfHost": { + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, "unassignedItemsBannerNotice": { "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." }, @@ -7938,8 +7973,8 @@ } } }, - "deleteProviderWarningDesc": { - "message": "You must unlink all clients before you can delete $ID$", + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", "placeholders": { "id": { "content": "$1", @@ -7964,5 +7999,93 @@ }, "errorAssigningTargetFolder": { "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations":{ + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/en_GB/messages.json b/apps/web/src/locales/en_GB/messages.json index 152c6a89e1..bf746dd0d5 100644 --- a/apps/web/src/locales/en_GB/messages.json +++ b/apps/web/src/locales/en_GB/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/en_IN/messages.json b/apps/web/src/locales/en_IN/messages.json index d4d22f6aba..3343910c8a 100644 --- a/apps/web/src/locales/en_IN/messages.json +++ b/apps/web/src/locales/en_IN/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without 2FA found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/eo/messages.json b/apps/web/src/locales/eo/messages.json index 899558160f..4f9ffd2f0f 100644 --- a/apps/web/src/locales/eo/messages.json +++ b/apps/web/src/locales/eo/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Trovitaj Nesekurigitaj Retejoj" }, - "unsecuredWebsitesFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo kun nesekurigitaj URI-oj. Vi devas ŝanĝi ilian URI-skemon al https: // se la retejo permesas ĝin.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Ensalutoj Sen 2FA Trovitaj" }, - "inactive2faFoundDesc": { - "message": "Ni trovis $COUNT$ retejon (j) en via trezorejo, kiu eble ne estas agordita kun dufakta aŭtentokontrolo (laŭ 2fa.directory). Por plue protekti ĉi tiujn kontojn, vi devas ebligi dufaktoran aŭtentikigon.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Trovitaj Pasvortoj Trovitaj" }, - "exposedPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo, kiuj havas pasvortojn elmontritajn en konataj rompo de datumoj. Vi devas ŝanĝi ilin por uzi novan pasvorton.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Malfortaj Pasvortoj Trovitaj" }, - "weakPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ erojn en via trezorejo kun pasvortoj ne fortaj. Vi devas ĝisdatigi ilin por uzi pli fortajn pasvortojn.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reuzitaj Pasvortoj Trovitaj" }, - "reusedPasswordsFoundDesc": { - "message": "Ni trovis $COUNT$ pasvortojn reuzatajn en via trezorejo. Vi devas ŝanĝi ilin al unika valoro.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/es/messages.json b/apps/web/src/locales/es/messages.json index 78b051d588..338d172bb7 100644 --- a/apps/web/src/locales/es/messages.json +++ b/apps/web/src/locales/es/messages.json @@ -579,7 +579,7 @@ "message": "Acceso" }, "accessLevel": { - "message": "Access level" + "message": "Nivel de acceso" }, "loggedOut": { "message": "Sesión terminada" @@ -1359,11 +1359,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " en su lugar.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": " en su lugar. Es posible que tenga que esperar hasta que su administrador confirme la pertenencia a su organización.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sitios web no seguros encontrados" }, - "unsecuredWebsitesFoundDesc": { - "message": "Hemos encontrado $COUNT$ elemento(s) en su caja fuerte con URIs no seguras. Si el sitio web lo permite debe cambiar su esquema URI a https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con URIs no seguros. Deberías cambiar su esquema URI a https:// si el sitio web lo permite.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Inicios de sesión sin 2FA encontrados" }, - "inactive2faFoundDesc": { - "message": "Hemos encontrado $COUNT$ sitios web en tu bóveda que pueden no estar configurados con inicio de sesión en dos pasos (de acuerdo a 2fa. irectory). Para proteger aún más estas cuentas, debe configurar el inicio de sesión en dos pasos.", + "inactive2faFoundReportDesc": { + "message": "Hemos encontrado $COUNT$ sitio(s) web en su $VAULT$ que pueden no estar configurados con inicio de sesión en dos pasos (según 2fa.directory). Para proteger aún más estas cuentas, debe configurar el inicio de sesión en dos pasos.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Contraseñas comprometidas encontradas" }, - "exposedPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ elementos en su caja fuerte que tienen contraseñas que fueron comprometidas en filtraciones de datos conocidas. Debe cambiarlos para utilizar una contraseña nueva.", + "exposedPasswordsFoundReportDesc": { + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ que tienen contraseñas que fueron expuestas en violaciones de datos conocidas. Deberías cambiarlas para utilizar una contraseña nueva.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Contraseñas débiles encontradas" }, - "weakPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ elemento(s) en su caja fuerte con contraseñas que no son fuertes. Se deben actualizar para usar contraseñas más fuertes.", + "weakPasswordsFoundReportDesc": { + "message": "Hemos encontrado $COUNT$ elementos en su $VAULT$ con contraseñas que no son seguras. Debe actualizarlos para utilizar contraseñas más seguras.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Contraseñas reutilizadas encontradas" }, - "reusedPasswordsFoundDesc": { - "message": "Hemos encontrado $COUNT$ contraseña(s) que están siendo reutilizadas en su caja fuerte. Debe cambiarlas a un valor único.", + "reusedPasswordsFoundReportDesc": { + "message": "Hemos encontrado $COUNT$ contraseñas que están siendo reutilizadas en tu $VAULT$. Deberías cambiarlas por un valor único.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -4951,13 +4971,13 @@ "message": "Cree una nueva organización de clientes que estará asociada a usted como proveedor. Usted poddrá acceder y gestionar esta organización." }, "newClient": { - "message": "New client" + "message": "Nuevo cliente" }, "addExistingOrganization": { "message": "Añadir una organización existente" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Añadir nueva organización" }, "myProvider": { "message": "Mi proveedor" @@ -5213,7 +5233,7 @@ "message": "Set a unique SP entity ID" }, "spUniqueEntityIdDesc": { - "message": "Generate an identifier that is unique to your organization" + "message": "Genera un identificador único para su organización" }, "idpEntityId": { "message": "ID de la entidad" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder acceso a las colecciones añadiéndolas a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Otorgar acceso a todas las colecciones actuales y futuras." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/et/messages.json b/apps/web/src/locales/et/messages.json index fdb656f31e..d2ea371f43 100644 --- a/apps/web/src/locales/et/messages.json +++ b/apps/web/src/locales/et/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Leiti ebaturvalisi veebilehti" }, - "unsecuredWebsitesFoundDesc": { - "message": "Leidsime hoidlast $COUNT$ ebaturvalist veebilehte.\nKui võimalik, soovitame nende veebilehtede alguse tungivalt https:// -ks muuta. ", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Kaheastmelise kinnituseta kontod" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Avastatud on lekkinud paroole" }, - "exposedPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ kirjet, millede paroolid on teadaolevate andmelekete tagajärjel avalikustatud. Soovitame tungivalt need paroolid ära vahetada.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Avastatud on nõrgad paroolid" }, - "weakPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ kirjet, milledel on nõrgad paroolid. Soovitame tungivalt need paroolid tugevamate vastu välja vahetada.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Leiti korduvalt kasutatud paroole" }, - "reusedPasswordsFoundDesc": { - "message": "Leidsime sinu hoidlast $COUNT$ parooli, mis on kasutusel rohkem kui üks kord Soovitame need paroolid unikaalseteks muuta.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/eu/messages.json b/apps/web/src/locales/eu/messages.json index 0125ceec25..9b208f945a 100644 --- a/apps/web/src/locales/eu/messages.json +++ b/apps/web/src/locales/eu/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Webgune ez seguruak aurkituak" }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ artikulu aurkitu d(it)ugu kutxa gotorrean bermatuta ez dauden URI-ekin. Zure URI eskema https://-ra aldatu behar duzu, web guneak onartzen badu.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA-rik gabeko saio hasierak aurkituak" }, - "inactive2faFoundDesc": { - "message": "$COUNT$ webgune aurkitu ditugu zure kutxa gotorrean bi urratseko autentifikazioarekin konfiguratu ezin direnak (twofactorauth.org-en arabera). Kontu horiek babesteko, bi urratseko autentifikazioa gaitu behar duzu.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Ikusgai dauden pasahitzak aurkituak" }, - "exposedPasswordsFoundDesc": { - "message": "Kutxa gotorrean $COUNT$ elementuren pasahitz daude, datu bortxaketa ezagunetan erakutsi direnak. Aldatu beharko zenituzke eta pasahitz berria erabili.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Pasahitz ahulak aurkituak" }, - "weakPasswordsFoundDesc": { - "message": "Zure kutxa gotorrean $COUNT$ elementu aurkitu ditugu pasahitz ahulekin. Eguneratu egin behar dituzu pasahitz seguruagoak erabiltzeko.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Pasahitz berrerabiliak aurkituak" }, - "reusedPasswordsFoundDesc": { - "message": "Zure kutxa gotorrean $COUNT$ elementu aurkitu ditugu pasahitz berrerabiliekin. Aldatu beharko zenituzke pasahitz bakarrak erabiltzeko.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fa/messages.json b/apps/web/src/locales/fa/messages.json index e91ea40d05..45a32b2011 100644 --- a/apps/web/src/locales/fa/messages.json +++ b/apps/web/src/locales/fa/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "وب‌سایت های نا امن پیدا شد" }, - "unsecuredWebsitesFoundDesc": { - "message": "ما $COUNT$ مورد را با نشانی‌های اینترنتی نا امن در خزانه شما پیدا کردیم. اگر وب‌سایت اجازه می‌دهد، باید طرح نشانی اینترنتی آن‌ها را به //:https‌ تغییر دهید.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "ورود‌های بدون ورود دو مرحله ای یافت شد" }, - "inactive2faFoundDesc": { - "message": "ما $COUNT$ وب‌سایت را در گاوصندوق شما پیدا کردیم که ممکن است با ورود دو مرحله‌ای پیکربندی نشده باشند (بر اساس 2fa.directory). برای محافظت بیشتر از این حساب‌ها، باید ورود دو مرحله ای را تنظیم کنید.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "کلمه‌های عبور افشا شده یافت شد" }, - "exposedPasswordsFoundDesc": { - "message": "ما $COUNT$ موردی را در خزانه شما پیدا کردیم که دارای کلمه‌های عبوری هستند که در نقض‌های اطلاعاتی شناخته شده افشا شده‌اند. شما باید آن‌ها را تغییر دهید تا از یک کلمه عبور جدید استفاده کنید.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "کلمه‌های عبور ضعیف پیدا شد" }, - "weakPasswordsFoundDesc": { - "message": "ما $COUNT$ مورد با کلمه‌های عبوری که قوی نیستند در گاوصندوق شما پیدا کردیم. شما باید آن‌ها را به‌روز کنید تا از کلمه‌های عبور قوی تر استفاده کنید.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "کلمه‌های عبور مجدد استفاده شده یافت شد" }, - "reusedPasswordsFoundDesc": { - "message": "ما $COUNT$ کلمه عبور پیدا کردیم که در گاوضندوق شما دوباره استفاده می‌شود. شما باید آن‌ها را به یک مقدار منحصر به فرد تغییر دهید.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "با افزودن مجموعه‌ها به این گروه، اجازه دسترسی به مجموعه‌ها را بدهید." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "اجازه دسترسی به تمام مجموعه‌های فعلی و آینده." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fi/messages.json b/apps/web/src/locales/fi/messages.json index 18fa9c15fb..eb0e93114b 100644 --- a/apps/web/src/locales/fi/messages.json +++ b/apps/web/src/locales/fi/messages.json @@ -630,7 +630,7 @@ "message": "Suojausavain ei kelpaa. Yritä uudelleen." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "Kaksivaiheista salausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." + "message": "Kaksivaiheista suojausavainkirjautumista ei tueta. Päivitä sovellus kirjautuaksesi sisään." }, "loginWithPasskeyInfo": { "message": "Käytä generoitua suojausavainta, joka kirjaa sinut automaattisesti sisään ilman salasanaa. Henkilöllisyytesi vahvistetaan kasvojen tunnistuksen tai sormenjäljen kataisilla biometrisillä tiedoilla, tai jollakin muulla FIDO2-suojausratkaisulla." @@ -648,10 +648,10 @@ "message": "Pidä tämä ikkuna avoinna ja seuraa selaimesi opasteita." }, "errorCreatingPasskey": { - "message": "Virhe suojausavaimen luonnissa" + "message": "Virhe luotaessa suojausavainta" }, "errorCreatingPasskeyInfo": { - "message": "Suojausavaimesi luonnissa kohdattiin ongelma." + "message": "Suojausavaintasi luotaessa kohdattiin ongelma." }, "passkeySuccessfullyCreated": { "message": "Suojausavain on luotu" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Suojaamattomia verkkosivustoja löytyi" }, - "unsecuredWebsitesFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta suojaamattomilla URI-osoitteilla. Sinun tulisi muuttaa niiden URI suojattuun \"https://\" -muotoon, jos sivusto tukee sitä.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta suojaamattomilla URI-osoitteilla. Nämä tulisi muuttaa suojattuun \"https://\" -muotoon, jos sivustot sen mahdollistavat.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Löytyi kirjautumistietoja, joille ei ole määritetty kaksivaiheista kirjautumista" }, - "inactive2faFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ sivustoa, joita ei ehkä ole määritetty käyttämään kaksivaiheista kirjautumista (2fa.directory-sivuston mukaan). Suojataksesi nämä tilit paremmin, sinun tulisi määrittää niille kaksivaiheinen kirjautuminen.", + "inactive2faFoundReportDesc": { + "message": "Löysimme holv(e)istasi $COUNT$ sivustoa, joita ei ehkä ole määritetty käyttämään kaksivaiheista tunnistautumista (2fa.directory-sivuston mukaan). Nämä tilit tulisi suojata paremmin määrittämällä niille kaksivaiheinen tunnistautuminen.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Paljastuneita salasanoja löytyi" }, - "exposedPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta, jotka sisältävät tunnetuissa tietovuodoissa paljastuneita salasanoja. Näiden palveluiden salasanat tulisi vaihtaa.", + "exposedPasswordsFoundReportDesc": { + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat ovat paljastuneet tunnetuissa tietovuodoissa. Nämä salasanat tulisi vaihtaa.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Heikkoja salasanoja löytyi" }, - "weakPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ kohdetta, joiden salasanat eivät ole vahvoja. Nämä tulisi korvata vahvemmilla salasanoilla.", + "weakPasswordsFoundReportDesc": { + "message": "Löysimme holv(e)istasi $COUNT$ kohdetta, joiden salasanat eivät ole vahvoja. Nämä tulisi korvata vahvemmilla salasanoilla.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Toistuvia salasanoja löytyi" }, - "reusedPasswordsFoundDesc": { - "message": "Löysimme holvistasi $COUNT$ toistuvasti käytettyä salasanaa. Ne tulisi vaihtaa yksilöllisiksi.", + "reusedPasswordsFoundReportDesc": { + "message": "Löysimme holv(e)istasi $COUNT$ toistuvasti käytettyä salasanaa. Ne tulisi korvata yksilöllisillä salasanoilla.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2799,7 +2819,7 @@ "message": "Verkkoholvi" }, "cli": { - "message": "komentorivi" + "message": "Komentorivi" }, "bitWebVault": { "message": "Bitwarden Verkkoholvi" @@ -4948,10 +4968,10 @@ "message": "Uusi asiakasorganisaatio" }, "newClientOrganizationDesc": { - "message": "Luo uusi asiakasorganisaatio, joka liitetään sinuun toimittajana. Voit käyttää ja hallita tätä organisaatiota." + "message": "Luo uusi asiakasorganisaatio, jonka toimittajaksi sinut määritetään. Voit käyttää ja hallita tätä organisaatiota." }, "newClient": { - "message": "Uusi pääte" + "message": "Uusi asiakas" }, "addExistingOrganization": { "message": "Lisää olemassa oleva organisaatio" @@ -6238,7 +6258,7 @@ "description": "Title for creating a new project." }, "softDeleteSecretWarning": { - "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integrointeihin.", + "message": "Salaisuuksien poistaminen voi vaikuttaa olemassa oleviin integraatioihin.", "description": "Warns that deleting secrets can have consequences on integrations" }, "softDeletesSuccessToast": { @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Myönnä käyttöoikeudet kokoelmiin lisäämällä heidät tähän ryhmään." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Voit määrittää vain hallitsemiasi kokoelmia." + }, "accessAllCollectionsDesc": { "message": "Myönnä käyttöoikeudet kaikkiin nykyisiin ja tuleviin kokoelmiin" }, @@ -7071,7 +7094,7 @@ "description": "The individual description shown to the user when the user doesn't have access to delete a project." }, "smProjectsDeleteBulkConfirmation": { - "message": "Seuraavien projektien poistaminen ei ole mahdollista. Haluatko jatkaa?", + "message": "Seuraavia projekteja ei ole mahdollista poistaa. Haluatko jatkaa?", "description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects." }, "updateKdfSettings": { @@ -7217,7 +7240,7 @@ "message": "Tilille ei ole asetettu pääsalasanaa" }, "removeOrgUserNoMasterPasswordDesc": { - "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta hänen omalle tililleen. Haluatko varmasti jatkaa?", + "message": "Käyttäjän $USER$ poistaminen asettamatta hänen tililleen pääsalasanaa voi estää häntä kirjautumasta heidän omalle tililleen. Haluatko varmasti jatkaa?", "placeholders": { "user": { "content": "$1", @@ -7484,7 +7507,7 @@ "message": "Laajennuksella voit tallentaa kirjautumistietoja ja automaattitäyttää lomakkeita avaamatta verkkosovellusta." }, "projectAccessUpdated": { - "message": "Projektin käyttöoikeudet on muutettu" + "message": "Projektin käyttöoikeuksia muutettiin" }, "unexpectedErrorSend": { "message": "Odottamaton virhe ladattaessa Sendiä. Yritä myöhemmin uudelleen." @@ -7514,7 +7537,7 @@ "message": "Ylläpitäjät voivat käyttää ja hallinnoida kokoelmia." }, "serviceAccountAccessUpdated": { - "message": "Palvelutilin oikeuksia muutettiin" + "message": "Palvelutilin käyttöoikeuksia muutettiin" }, "commonImportFormats": { "message": "Yleiset muodot", @@ -7595,7 +7618,7 @@ "message": "Ilmainen 1 vuoden ajan" }, "newWebApp": { - "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Lue lisää siitä, mikä on muuttunut." + "message": "Tervetuloa uuteen, entistä parempaan verkkosovellukseen. Katso mikä on muuttunut." }, "releaseBlog": { "message": "Lue julkaisublogia" @@ -7606,8 +7629,11 @@ "providerPortal": { "message": "Toimittajaportaali" }, + "success": { + "message": "Onnistui" + }, "viewCollection": { - "message": "Tarkastele kokoelmaa" + "message": "Näytä kokoelma" }, "restrictedGroupAccess": { "message": "Et voi lisätä itseäsi ryhmiin." @@ -7637,7 +7663,7 @@ "message": "Kokoelmat määritettiin" }, "bulkCollectionAssignmentWarning": { - "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Et voi päivittää näistä $READONLY_COUNT$ kohdetta, koska käyttöoikeutesi eivät salli muokkausta.", + "message": "Olet valinnut $TOTAL_COUNT$ kohdetta. Näistä $READONLY_COUNT$ et voi muuttaa, koska käyttöoikeutesi eivät salli muokkausta.", "placeholders": { "total_count": { "content": "$1", @@ -7665,7 +7691,7 @@ "message": "Jäljellä" }, "unlinkOrganization": { - "message": "Irrota organisaatio" + "message": "Poista organisaatioliitos" }, "manageSeats": { "message": "HALLITSE KÄYTTÄJÄPAIKKOJA" @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Konetilin oikeuksia muutettiin" }, - "unassignedItemsBanner": { - "message": "Huomautus: Organisaatioiden kokoelmiin määrittämättömät kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "restrictedGroupAccessDesc": { + "message": "Et voi lisätä itseäsi ryhmään." }, "unassignedItemsBannerSelfHost": { - "message": "Huomautus: 2.5.2024 alkaen kokoelmiin määrittämättömät organisaatioiden kohteet eivät enää näy laitteiden \"Kaikki holvit\" -näkymissä, vaan ne ovat nähtävissä vain Hallintapaneelista. Määritä kohteet kokoelmiin Hallintapaneelista, jotta ne ovat jatkossakin käytettävissä kaikilta laitteilta." + "message": "Huomioi: Alkaen 2. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerNotice": { + "message": "Huomioi: Määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Huomioi: Alkaen 16. toukokuuta 2024, määrittämättömät organisaatiokohteet eivät enää näy laitteidesi \"Kaikki holvit\" -näkymissä, vaan ne ovat käytettävissä vain Hallintapaneelista." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Määritä nämä kohteet kokoelmaan", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", jotta ne näkyvät.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Poista toimittaja" + }, + "deleteProviderConfirmation": { + "message": "Toimittajan poisto on pysyvää ja peruuttamatonta. Vahvista palveluntarjoajan ja kaikkien siihen liittyvien tietojen poisto syöttämällä pääsalasanasi." + }, + "deleteProviderName": { + "message": "Toimittajaa $ID$ ei ole mahdollista poistaa", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Toimittaja poistettiin" + }, + "providerDeletedDesc": { + "message": "Toimittaja ja kaikki siihen liittyvät tiedot on poistettu." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Olet pyytänyt tämän toimittajan poistoa. Vahvista alla olevalla painikeella." + }, + "deleteProviderWarning": { + "message": "Toimittajasi poisto on pysyvä toimenpide, eikä sen peruminen ole mahdollista." + }, + "errorAssigningTargetCollection": { + "message": "Virhe määritettäessä kohdekokoelmaa." + }, + "errorAssigningTargetFolder": { + "message": "Virhe määritettäessä kohdekansiota." + }, + "integrationsAndSdks": { + "message": "Integraatiot ja SDK:t", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integraatiot" + }, + "integrationsDesc": { + "message": "Synkronoi salaisuudet automaattisesti Bitwardenin Salaisuushallinnan ja ulkopuolisen palvelun välillä." + }, + "sdks": { + "message": "SDK:t" + }, + "sdksDesc": { + "message": "Bitwardenin Salaisuushallinnan SDK:n avulla voit kehittää omia sovelluksiasi seuraavilla ohjelmointikielillä." + }, + "setUpGithubActions": { + "message": "Määritä GitHub Actions" + }, + "setUpGitlabCICD": { + "message": "Määritä GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Määritä Ansible" + }, + "cSharpSDKRepo": { + "message": "Näytä C#-arkisto" + }, + "cPlusPlusSDKRepo": { + "message": "Näytä C++-arkisto" + }, + "jsWebAssemblySDKRepo": { + "message": "Näytä JS WebAssembly -arkisto" + }, + "javaSDKRepo": { + "message": "Näytä Java-arkisto" + }, + "pythonSDKRepo": { + "message": "Näytä Python-arkisto" + }, + "phpSDKRepo": { + "message": "Näytä php-arkisto" + }, + "rubySDKRepo": { + "message": "Näytä Ruby-arkisto" + }, + "goSDKRepo": { + "message": "Näytä Go-arkisto" + }, + "createNewClientToManageAsProvider": { + "message": "Luo uusi asiakasorganisaatio, jota hallitset toimittajana. Uudet käyttäjäpaikat näkyvät seuraavalla laskutusjaksolla." + }, + "selectAPlan": { + "message": "Valitse tilaus" + }, + "thirtyFivePercentDiscount": { + "message": "35 %:n alennus" + }, + "monthPerMember": { + "message": "kuukaudessa/jäsen" + }, + "seats": { + "message": "Käyttäjäpaikat" + }, + "addOrganization": { + "message": "Lisää organisaatio" + }, + "createdNewClient": { + "message": "Uuden asiakkaan luonti onnistui." + }, + "noAccess": { + "message": "Käyttöoikeutta ei ole" + }, + "collectionAdminConsoleManaged": { + "message": "Tämä kokoelma on käytettävissä vain hallintakonsolista" + }, + "organizationOptionsMenu": { + "message": "Näytä/piilota organisaatiovalikko" + }, + "vaultItemSelect": { + "message": "Valitse holvin kohde" + }, + "collectionItemSelect": { + "message": "Valitse kokoelman kohde" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Hallitse laskutusta Toimittajaportaaliista" } } diff --git a/apps/web/src/locales/fil/messages.json b/apps/web/src/locales/fil/messages.json index 2cf9eb3b7e..e3806b6541 100644 --- a/apps/web/src/locales/fil/messages.json +++ b/apps/web/src/locales/fil/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "May nakitang mga website na hindi ligtas" }, - "unsecuredWebsitesFoundDesc": { - "message": "May $COUNT$ item sa vault mo na gumagamit ng hindi ligtas na URI. Dapat gawing https:// ang URI scheme nito kung maa-access mo ang website gamit nito.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "May mga login na nakasara ang dalawang-hakbang na pag-log in" }, - "inactive2faFoundDesc": { - "message": "May $COUNT$ website sa vault mo na maaaring hindi pa gumagamit ng dalawang-hakbang na pag-log in (ayon sa 2fa.directory). I-set up ang dalawang-hakbang na pag-log in sa mga account na ito para sa karagdagang proteksyon.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "May nakitang mga nakompromisong password" }, - "exposedPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may password na nakompromiso sa mga naitalang data breach. Kailangan mo silang baguhin.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "May mga mahinang password" }, - "weakPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may mahinang password. Dapat mo silang palakasin.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "May mga naulit na password" }, - "reusedPasswordsFoundDesc": { - "message": "May $COUNT$ item sa vault mo na may password na pinapaulit-ulit. Kailangan mo silang gawing unique.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Magbigay ng access sa mga koleksyon sa pamamagitan ng pagdaragdag ng mga ito sa grupong ito." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Magbigay ng access sa lahat ng kasalukuyan at hinaharap na mga koleksyon." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/fr/messages.json b/apps/web/src/locales/fr/messages.json index 27b529442c..eac5e78d87 100644 --- a/apps/web/src/locales/fr/messages.json +++ b/apps/web/src/locales/fr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites web non sécurisés trouvés" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre avec des URI non sécurisés. Vous devriez remplacer leur schéma URI par https:// si le site Web le permet.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nous avons $COUNT$ éléments dans $VAULT$ avec des URI non sécurisées. Vous devriez modifier leur schéma URI en https:// si le site le permet.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Identifiants sans authentification à deux facteurs trouvés" }, - "inactive2faFoundDesc": { - "message": "Nous avons trouvé $COUNT$ site(s) web dans votre coffre peut-être non configuré(s) avec l'authentification à deux facteurs (selon 2fa.directory). Pour protéger davantage ces comptes, vous devriez mettre en place l'authentification à deux facteurs.", + "inactive2faFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ site(s) web dans $VAULT$ qui ne sont peut-être pas configurés avec une connexion en deux étapes (selon le 2fa.directory). Pour protéger davantage ces comptes, vous devez configurer une connexion en deux étapes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Mots de passe exposés trouvés" }, - "exposedPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre qui ont des mots de passe qui ont été exposés dans des fuites de données connues. Vous devriez les changer pour utiliser un nouveau mot de passe.", + "exposedPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ éléments dans $VAULT$ qui ont des mots de passe qui ont été exposés dans des fuites de données connues. Vous devriez les changer pour utiliser un nouveau mot de passe.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Mots de passe faibles trouvés" }, - "weakPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ éléments dans votre coffre avec des mots de passe qui ne sont pas forts. Vous devriez les mettre à jour pour utiliser des mots de passe plus forts.", + "weakPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ éléments dans $VAULT$ avec des mots de passe faibles. Vous devriez les mettre à jour pour utiliser des mots de passe plus forts.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Mots de passe réutilisés trouvés" }, - "reusedPasswordsFoundDesc": { - "message": "Nous avons trouvé $COUNT$ mots de passe qui sont réutilisés dans votre coffre. Vous devriez les changer pour utiliser des mots de passe différents.", + "reusedPasswordsFoundReportDesc": { + "message": "Nous avons trouvé $COUNT$ mots de passe qui sont réutilisés dans $VAULT$. Vous devriez les changer pour utiliser des mots de passe uniques.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Accorder l'accès aux collections en les ajoutant à ce groupe." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Vous ne pouvez affecter que les collections que vous gérez." + }, "accessAllCollectionsDesc": { "message": "Accorder l'accès à toutes les collections actuelles et futures." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portail fournisseur" }, + "success": { + "message": "Succès" + }, "viewCollection": { "message": "Afficher la Collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Accès au compte machine mis à jour" }, - "unassignedItemsBanner": { - "message": "Remarque : les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "restrictedGroupAccessDesc": { + "message": "Vous ne pouvez pas vous ajouter vous-même à un groupe." }, "unassignedItemsBannerSelfHost": { - "message": "Remarque : au 2 mai 2024, les éléments d'organisation non assignés ne sont plus visibles dans votre vue Tous les coffres sur tous les appareils et sont uniquement accessibles via la Console d'administration. Assignez ces éléments à une collection à partir de la Console d'administration pour les rendre visibles." + "message": "Remarque : le 2 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans votre vue Tous les coffres sur les appareils et seront uniquement accessibles via la Console Admin. Assignez ces éléments à une collection à partir de la Console Admin pour les rendre visibles." + }, + "unassignedItemsBannerNotice": { + "message": "Remarque : Les éléments d'organisation non assignés ne sont plus visibles dans la vue Tous les coffres sur les appareils et ne sont maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Remarque : À partir du 16 mai 2024, les éléments d'organisation non assignés ne seront plus visibles dans la vue Tous les coffres sur les appareils et ne seront maintenant accessibles que via la Console Admin." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assigner ces éléments à une collection depuis", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "pour les rendre visibles.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Supprimer le fournisseur" + }, + "deleteProviderConfirmation": { + "message": "La suppression d'un fournisseur est permanente et irréversible. Entrez votre mot de passe principal pour confirmer la suppression du fournisseur et de toutes les données associées." + }, + "deleteProviderName": { + "message": "Impossible de supprimer $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fournisseur supprimé" + }, + "providerDeletedDesc": { + "message": "Le fournisseur et toutes les données associées ont été supprimés." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Vous avez demandé à supprimer ce fournisseur. Utilisez le bouton ci-dessous pour confirmer." + }, + "deleteProviderWarning": { + "message": "La suppression de votre fournisseur est permanente. Elle ne peut pas être annulée." + }, + "errorAssigningTargetCollection": { + "message": "Erreur lors de l'assignation de la collection cible." + }, + "errorAssigningTargetFolder": { + "message": "Erreur lors de l'assignation du dossier cible." + }, + "integrationsAndSdks": { + "message": "Intégrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Intégrations" + }, + "integrationsDesc": { + "message": "Synchroniser automatiquement les secrets à partir du Secrets Manager de Bitwarden vers un service tiers." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Utilisez Bitwarden Secrets Manager SDK dans les langages de programmation suivants pour construire vos propres applications." + }, + "setUpGithubActions": { + "message": "Configurer Github Actions" + }, + "setUpGitlabCICD": { + "message": "Configurer GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configurer Ansible" + }, + "cSharpSDKRepo": { + "message": "Afficher le dépôt C#" + }, + "cPlusPlusSDKRepo": { + "message": "Afficher le dépôt C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Afficher le dépôt JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Afficher le dépôt Java" + }, + "pythonSDKRepo": { + "message": "Afficher le dépôt Python" + }, + "phpSDKRepo": { + "message": "Afficher le dépôt php" + }, + "rubySDKRepo": { + "message": "Afficher le dépôt Ruby" + }, + "goSDKRepo": { + "message": "Afficher le dépôt Go" + }, + "createNewClientToManageAsProvider": { + "message": "Créez une nouvelle organisation de clients à gérer en tant que Fournisseur. Des sièges supplémentaires seront reflétés lors du prochain cycle de facturation." + }, + "selectAPlan": { + "message": "Sélectionnez un plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% de réduction" + }, + "monthPerMember": { + "message": "mois par membre" + }, + "seats": { + "message": "Licences" + }, + "addOrganization": { + "message": "Ajouter une organisation" + }, + "createdNewClient": { + "message": "Nouveau client créé avec succès" + }, + "noAccess": { + "message": "Aucun accès" + }, + "collectionAdminConsoleManaged": { + "message": "Cette collection n'est accessible qu'à partir de la Console Admin" + }, + "organizationOptionsMenu": { + "message": "Afficher/masquer le Menu de l'Organisation" + }, + "vaultItemSelect": { + "message": "Sélectionner un élément du coffre" + }, + "collectionItemSelect": { + "message": "Sélectionner un élément de la collection" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gérer la facturation depuis le Portail Fournisseur" } } diff --git a/apps/web/src/locales/gl/messages.json b/apps/web/src/locales/gl/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/gl/messages.json +++ b/apps/web/src/locales/gl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/he/messages.json b/apps/web/src/locales/he/messages.json index fd42338a28..c448a25e59 100644 --- a/apps/web/src/locales/he/messages.json +++ b/apps/web/src/locales/he/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "נמצאו אתרים לא מאובטחים" }, - "unsecuredWebsitesFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך המכילים כתובות לא מאובטחות. אנו ממליצים לשנות את הכתובות לתחילית https:// אם האתר מאפשר זאת.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "נמצאו פרטי כניסות שלא פעילה בהן אופציית 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "נמצאו סיסמאות שנחשפו" }, - "exposedPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך שיש להם סיסמאות שנחשפו בפרצות אבטחה. מומלץ לשנות אותן וליצור סיסמאות חדשות.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "נמצאו סיסמאות חלשות" }, - "weakPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ פריטים בכספת שלך עם סיסמאות חלשות. מומלץ להשתמש בסיסמאות חזקות יותר.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "נמצאו סיסמאות משומשות" }, - "reusedPasswordsFoundDesc": { - "message": "מצאנו $COUNT$ סיסמאות משומשות בכספת שלך. כדאי שתשנה אותם כך שלכל פריט תהיה סיסמה ייחודית.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hi/messages.json b/apps/web/src/locales/hi/messages.json index f0487066d6..5e0d3b8c29 100644 --- a/apps/web/src/locales/hi/messages.json +++ b/apps/web/src/locales/hi/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hr/messages.json b/apps/web/src/locales/hr/messages.json index 20a62053f0..6eac81da08 100644 --- a/apps/web/src/locales/hr/messages.json +++ b/apps/web/src/locales/hr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Pronađena neosigurana web mjesta" }, - "unsecuredWebsitesFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru koje koriste neosigurane URI-je (http://). Ako web mjesto omogućuje trebalo bi URI-je promijeniti na https://", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Prijave na kojima nije omogućena dvostruka autentifikacija" }, - "inactive2faFoundDesc": { - "message": "Pronašli smo $COUNT$ web mjesta u tvom trezoru za koje nije omogućena prijava dvostrukom autentifikacijom (izvor: 2fa.directory). Za bolju zaštitu ovih računa, potrebno je omogućiti dvostruku autentifikaciju.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Pronađene izložene lozinke" }, - "exposedPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru koje imaju lozinke koje su otkrivene prilikom znanih curenja podataka. Trebalo bi ih zamijentii novim lozinkama.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Pronađene slabe lozinke" }, - "weakPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ stavki u tvom trezoru s lozinkama koje nisu jake. Trebalo bi ih zamijeniti jakim lozinkama.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Pronađene iste lozinke" }, - "reusedPasswordsFoundDesc": { - "message": "Pronašli smo $COUNT$ istih lozinki u tvom trezoru. Trebalo bi ih zamijeniti jedinstvenim lozinkama.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Odobri pristup zbirkama dodavanjem u ovu grupu." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Odobri pristup svim postojećim i budućim zbirkama." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/hu/messages.json b/apps/web/src/locales/hu/messages.json index 35781b814a..1c99680f62 100644 --- a/apps/web/src/locales/hu/messages.json +++ b/apps/web/src/locales/hu/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Nem-biztonságos webhelyek találhatók." }, - "unsecuredWebsitesFoundDesc": { - "message": "$COUNT$ elem található a széfben nem-biztonságos URI-val. Ezeket URI sémáját célszerű módosítani https://-re.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$COUNT$ elem van a $VAULT$ széfben nem biztonságos URI-val. Az URI sémát célszeűrű módosítani https://-re, ha a webhely azt engedélyezi.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Kétlépések bejelentkezés nélküli bejelentkezések találhatók." }, - "inactive2faFoundDesc": { - "message": "$COUNT$ olyan webhelyet találtunk a széfben, amely nincs kétlépcsős hitelesítéssel konfigurálva (a 2fa.directory adatbázisa alapján). Ezen fiókok további védelme érdekében, javasolt a kétlépcsős hitelesítés használata.", + "inactive2faFoundReportDesc": { + "message": "$COUNT$ olyan webhelye(ke)t találtunk a $VAULT$ széfben, amely nincs kétlépcsős bejelentkezéssel konfigurálva (a 2fa.directory adatbázisa alapján). Ezen fiókok további védelme érdekében, célszerű a kétlépcsős bejelentkezés használata.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kiszivárgott jelszavak találhatók." }, - "exposedPasswordsFoundDesc": { - "message": "$COUNT$ elem található a széfben, amelyek érintve voltak ismert adatszivárgásban. Célszerű új jelszavakra lecserélni ezeket.", + "exposedPasswordsFoundReportDesc": { + "message": "$COUNT$ elem található a $VAULT$ széfben, amelyek érintve voltak ismert adatszivárgásban. Célszerű ezeket megváltoztatni új jelszó használatával.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Gyenge jelszavak találhatók." }, - "weakPasswordsFoundDesc": { - "message": "$COUNT$ gyenge jelszó van a széfben. Célszerű lenne ezeket lecserélni erősebb jelszóra.", + "weakPasswordsFoundReportDesc": { + "message": "$COUNT$ gyenge jelszót találtunk a $VAULT$ széfben. Célszerű lenne ezeknél erősebb jelszóra áttérni.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Újrahasznált jelszavak találhatók." }, - "reusedPasswordsFoundDesc": { - "message": "$COUNT$ újrahasznált jelszó van a széfben. Változtassuk meg ezeket egyedi értékűre.", + "reusedPasswordsFoundReportDesc": { + "message": "$COUNT$ többször is használt jelszót találtunk a $VAULT$ széfben. Célszerű ezeknél a jelszót egyedi jelszavakra cseréni.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Hozzáférés kiosztása gyűjteményekhez csoporthoz adásukkal." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Csak a saját kezelésű gyűjteményeket lehet hozzárendelni." + }, "accessAllCollectionsDesc": { "message": "Hozzáférés kiosztása az összes jelenlegi és jövőbeli gyűjteményhez." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Szolgáltató portál" }, + "success": { + "message": "Sikeres" + }, "viewCollection": { "message": "Gyűjtemény megtekintése" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "A gépi fiók elérése frissítésre került." }, - "unassignedItemsBanner": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "restrictedGroupAccessDesc": { + "message": "Nem adhadjuk magunkat a csoporthoz." }, "unassignedItemsBannerSelfHost": { - "message": "Megjegyzés: A nem hozzá rendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és mostantól csak a Felügyeleti konzolon keresztül érhetők el. Rendeljük ezeket az elemeket egy gyűjteményhez az Adminisztrátori konzolból, hogy láthatóvá tegyeük azokat." + "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Megjegyzés: A nem hozzárendelt szervezeti elemek már nem láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátori konzolon keresztül érhetők el." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Megjegyzés: 2024. május 16-tól a nem hozzárendelt szervezeti elemek többé nem lesznek láthatók az Összes széf nézetben a különböző eszközökön és csak az Adminisztrátoir konzolon keresztül lesznek elérhetők." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Rendeljük hozzá ezeket az elemeket a gyűjteményhez", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "a láthatósághoz.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "$ID$ törlése előtt le kell választani az összes klienst.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Hiba történt a célgyűjtemény hozzárendelése során." + }, + "errorAssigningTargetFolder": { + "message": "Hiba történt a célmappa hozzárendelése során." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Nincs hozzáférés." + }, + "collectionAdminConsoleManaged": { + "message": "Ez a gyűjtemény csak az adminisztrátori konzolról érhető el." + }, + "organizationOptionsMenu": { + "message": "Szervezeti menü váltás" + }, + "vaultItemSelect": { + "message": "Széf elem választás" + }, + "collectionItemSelect": { + "message": "Gyűjtemény elem választás" + }, + "manageBillingFromProviderPortalMessage": { + "message": "A számlázás kezelése a szolgáltatói portálon keresztül" } } diff --git a/apps/web/src/locales/id/messages.json b/apps/web/src/locales/id/messages.json index 4cf235eb16..0aacc90be7 100644 --- a/apps/web/src/locales/id/messages.json +++ b/apps/web/src/locales/id/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Situs Web Tidak Aman Ditemukan" }, - "unsecuredWebsitesFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda dengan URI yang tidak aman. Anda harus mengubah skema URI mereka menjadi https: // jika situs web mengizinkannya.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Login Tanpa Ditemukan 2FA" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Kata Sandi Terkena Ditemukan" }, - "exposedPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda yang memiliki sandi yang diketahui dalam pelanggaran data yang diketahui. Anda harus mengubahnya untuk menggunakan kata sandi baru.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Kata Sandi Lemah Ditemukan" }, - "weakPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ item di lemari besi Anda dengan sandi yang tidak kuat. Anda harus memperbaruinya untuk menggunakan kata sandi yang lebih kuat.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Kata Sandi yang Digunakan Kembali Ditemukan" }, - "reusedPasswordsFoundDesc": { - "message": "Kami menemukan $COUNT$ sandi yang digunakan kembali di lemari besi Anda. Anda harus mengubahnya menjadi nilai unik.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/it/messages.json b/apps/web/src/locales/it/messages.json index 1cdee4420d..5c3894d443 100644 --- a/apps/web/src/locales/it/messages.json +++ b/apps/web/src/locales/it/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Siti web non protetti trovati" }, - "unsecuredWebsitesFoundDesc": { + "unsecuredWebsitesFoundReportDesc": { "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con URI non protetti. Dovresti cambiare il loro schema URL in https:// se il sito lo consente.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Login senza verifica in due passaggi trovati" }, - "inactive2faFoundDesc": { + "inactive2faFoundReportDesc": { "message": "Abbiamo trovato $COUNT$ siti web nella tua cassaforte che potrebbero non essere configurati con la verifica in due passaggi (secondo 2fa.directory). Per proteggere ulteriormente questi account, abilita la verifica in due passaggi.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Password esposte trovate" }, - "exposedPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte contenenti password che sono state esposte in violazioni di dati. Dovresti modificarli per usare una nuova password.", + "exposedPasswordsFoundReportDesc": { + "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte che hanno password che sono state esposte a violazioni di dati note. Dovresti cambiarli per usare una nuova password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Password deboli trovate" }, - "weakPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con password deboli. Aggiornali usando password più sicure.", + "weakPasswordsFoundReportDesc": { + "message": "Abbiamo trovato $COUNT$ elementi nella tua cassaforte con password che non sono forti. Dovresti aggiornarli per usare password più forti.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Password riutilizzate trovate" }, - "reusedPasswordsFoundDesc": { - "message": "Abbiamo trovato $COUNT$ password riutilizzate nella tua cassaforte. Cambiale in modo che ognuna sia unica.", + "reusedPasswordsFoundReportDesc": { + "message": "Abbiamo trovato $COUNT$ password che vengono riutilizzate nella tua cassaforte. Dovresti cambiarli in un valore univoco.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Concedi accesso alle raccolte aggiungendo gli utenti a questo gruppo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Puoi assegnare solo le raccolte che gestisci." + }, "accessAllCollectionsDesc": { "message": "Concedi accesso a tutte le raccolte esistenti e future." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portale Fornitori" }, + "success": { + "message": "Successo" + }, "viewCollection": { "message": "Visualizza raccolta" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Accesso all'account macchina aggiornato" }, - "unassignedItemsBanner": { - "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + "restrictedGroupAccessDesc": { + "message": "Non puoi aggiungerti a un gruppo." }, "unassignedItemsBannerSelfHost": { "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione. Assegna questi elementi ad una raccolta dalla Console di amministrazione per renderli visibili." + }, + "unassignedItemsBannerNotice": { + "message": "Avviso: gli elementi dell'organizzazione non assegnati non sono più visibili nella visualizzazione Tutte le Cassaforti su tutti i dispositivi e sono ora accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Avviso: dal 2 maggio 2024, gli elementi dell'organizzazione non assegnati non saranno più visibili nella tua visualizzazione Tutte le Cassaforti su tutti i dispositivi e saranno accessibili solo tramite la Console di amministrazione." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assegna questi elementi ad una raccolta dalla", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "per renderli visibili.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Elimina fornitore" + }, + "deleteProviderConfirmation": { + "message": "La cancellazione di un fornitore è permanente e irreversibile. Inserisci la tua password principale per confermare l'eliminazione del fornitore e di tutti i dati associati." + }, + "deleteProviderName": { + "message": "Impossibile eliminare $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Devi scollegare tutti i client prima di poter eliminare $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornitore eliminato" + }, + "providerDeletedDesc": { + "message": "Il fornitore e tutti i dati associati sono stati eliminati." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Hai richiesto di eliminare il questo fornitore. Clicca qui sotto per confermare." + }, + "deleteProviderWarning": { + "message": "L'eliminazione del fornitore è permanente. Questa azione non è reversibile." + }, + "errorAssigningTargetCollection": { + "message": "Errore nell'assegnazione della raccolta di destinazione." + }, + "errorAssigningTargetFolder": { + "message": "Errore nell'assegnazione della cartella di destinazione." + }, + "integrationsAndSdks": { + "message": "Integrazioni e SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrazioni" + }, + "integrationsDesc": { + "message": "Sincronizza automaticamente i segreti da Bitwarden Secrets Manager a un servizio di terze parti." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Usa l'SDK di Bitwarden Secrets Manager nei seguenti linguaggi di programmazione per creare le tue applicazioni." + }, + "setUpGithubActions": { + "message": "Configura GitHub Actions" + }, + "setUpGitlabCICD": { + "message": "Configura GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configura Ansible" + }, + "cSharpSDKRepo": { + "message": "Visualizza la repository C#" + }, + "cPlusPlusSDKRepo": { + "message": "Visualizza la repository C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Visualizza la repository JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Visualizza la repository Java" + }, + "pythonSDKRepo": { + "message": "Visualizza la repository Python" + }, + "phpSDKRepo": { + "message": "Visualizza la repository php" + }, + "rubySDKRepo": { + "message": "Visualizza la repository Ruby" + }, + "goSDKRepo": { + "message": "Visualizza la repository Go" + }, + "createNewClientToManageAsProvider": { + "message": "Crea una nuova organizzazione cliente da gestire come fornitore. Gli slot aggiuntivi saranno riflessi nel prossimo ciclo di fatturazione." + }, + "selectAPlan": { + "message": "Seleziona un piano" + }, + "thirtyFivePercentDiscount": { + "message": "Sconto del 35%" + }, + "monthPerMember": { + "message": "mese per membro" + }, + "seats": { + "message": "Slot" + }, + "addOrganization": { + "message": "Aggiungi organizzazione" + }, + "createdNewClient": { + "message": "Nuovo cliente creato con successo" + }, + "noAccess": { + "message": "Nessun accesso" + }, + "collectionAdminConsoleManaged": { + "message": "Questa raccolta è accessibile solo dalla console di amministrazione" + }, + "organizationOptionsMenu": { + "message": "Attiva/Disattiva Menu Organizzazione" + }, + "vaultItemSelect": { + "message": "Seleziona elemento della cassaforte" + }, + "collectionItemSelect": { + "message": "Seleziona elemento della raccolta" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gestisci la fatturazione dal Portale del Fornitore" } } diff --git a/apps/web/src/locales/ja/messages.json b/apps/web/src/locales/ja/messages.json index 3561c81bd1..8bf56e6f65 100644 --- a/apps/web/src/locales/ja/messages.json +++ b/apps/web/src/locales/ja/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "セキュリティ保護されていないウェブサイトが見つかりました" }, - "unsecuredWebsitesFoundDesc": { - "message": "セキュアでないURIが$COUNT$個のアイテムで見つかりました。ウェブサイトが対応しているならば、URIをhttps://形式に変更すべきです。", + "unsecuredWebsitesFoundReportDesc": { + "message": "セキュアでない URI が $VAULT$ 内で $COUNT$ 個見つかりました。ウェブサイトが対応している場合は、URI を https:// 形式に変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "二段階認証を利用していないアイテムが見つかりました" }, - "inactive2faFoundDesc": { - "message": "保管庫に $COUNT$ 個のウェブサイトがあり、2段階認証でのログインが設定されていない可能性があります (2fa.directory によれば)。これらのアカウントをさらに保護するには、2段階認証のログインを設定する必要があります。", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ に $COUNT$ 個のウェブサイトがあり、(2fa.directory によれば) 2段階認証でのログインを設定できる可能性があります 。これらのアカウントをさらに保護するには、2段階認証のログインを設定してください。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "流出したパスワードが見つかりました" }, - "exposedPasswordsFoundDesc": { - "message": "既知のデータ流出で公開されていたパスワードが $COUNT$ 個のアイテムで見つかりました。これらは新しいパスワードへ変更すべきです。", + "exposedPasswordsFoundReportDesc": { + "message": "既知の流出済みパスワードが $VAULT$ 内の $COUNT$ 個のアイテムで見つかりました。これらは新しいパスワードへ変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "脆弱なパスワードが見つかりました" }, - "weakPasswordsFoundDesc": { - "message": "保管庫内に$COUNT$個の強力でないパスワードが見つかりました。もっと強力なパスワードへ更新すべきです。", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ 内に $COUNT$ 個の脆弱なパスワードが見つかりました。もっと強力なパスワードへ更新すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "再利用しているパスワードが見つかりました。" }, - "reusedPasswordsFoundDesc": { - "message": "$COUNT$個の再利用しているパスワードが見つかりました。それぞれ異なるパスワードへ変更すべきです。", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ 内に $COUNT$ 個の再利用しているパスワードが見つかりました。それぞれ異なるパスワードへ変更すべきです。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "このグループに追加してコレクションへのアクセスを許可します。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "現在および将来のすべてのコレクションへのアクセスを許可します。" }, @@ -7601,11 +7624,14 @@ "message": "リリースブログを読む" }, "adminConsole": { - "message": "管理者コンソール" + "message": "管理コンソール" }, "providerPortal": { "message": "プロバイダーポータル" }, + "success": { + "message": "成功" + }, "viewCollection": { "message": "コレクションを表示" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "マシンアカウントへのアクセス権限を更新しました" }, - "unassignedItemsBanner": { - "message": "注意: 割り当てられていない組織項目は、デバイス間のすべての保管庫のビューでは表示されなくなり、管理コンソールからのみアクセスできます。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示するようにできます。" + "restrictedGroupAccessDesc": { + "message": "あなた自身をグループに追加することはできません。" }, "unassignedItemsBannerSelfHost": { "message": "お知らせ:2024年5月2日に、 割り当てられていない組織アイテムはデバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。 管理コンソールからコレクションにこれらのアイテムを割り当てると、表示できるようになります。" + }, + "unassignedItemsBannerNotice": { + "message": "注意: 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューでは表示されなくなり、管理コンソールからのみアクセスできるようになりました。" + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "お知らせ:2024年5月16日に、 割り当てられていない組織アイテムは、デバイス間のすべての保管庫ビューに表示されなくなり、管理コンソールからのみアクセス可能になります。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "これらのアイテムのコレクションへの割り当てを", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "で実行すると表示できるようになります。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "プロバイダを削除" + }, + "deleteProviderConfirmation": { + "message": "プロバイダの削除は恒久的で元に戻せません。マスターパスワードを入力して、プロバイダと関連するすべてのデータの削除を確認してください。" + }, + "deleteProviderName": { + "message": "$ID$ を削除できません", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "$ID$ を削除するには、まずすべてのクライアントのリンクを解除してください。", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "プロバイダを削除しました" + }, + "providerDeletedDesc": { + "message": "プロバイダと関連するすべてのデータを削除しました。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "このプロバイダの削除をリクエストしました。下のボタンを使うと確認できます。" + }, + "deleteProviderWarning": { + "message": "プロバイダを削除すると元に戻すことはできません。" + }, + "errorAssigningTargetCollection": { + "message": "ターゲットコレクションの割り当てに失敗しました。" + }, + "errorAssigningTargetFolder": { + "message": "ターゲットフォルダーの割り当てに失敗しました。" + }, + "integrationsAndSdks": { + "message": "連携&SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "システム連携" + }, + "integrationsDesc": { + "message": "Bitwarden シークレットマネージャーのシークレットを、サードパーティーのサービスに自動的に同期します。" + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Bitwarden シークレットマネージャー SDK を以下のプログラミング言語で使用して、独自のアプリを構築できます。" + }, + "setUpGithubActions": { + "message": "Github アクションを設定" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD の設定" + }, + "setUpAnsible": { + "message": "Ansible を設定" + }, + "cSharpSDKRepo": { + "message": "C# リポジトリを表示" + }, + "cPlusPlusSDKRepo": { + "message": "C++ リポジトリを表示" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly リポジトリを表示" + }, + "javaSDKRepo": { + "message": "Java リポジトリを表示" + }, + "pythonSDKRepo": { + "message": "Python リポジトリを表示" + }, + "phpSDKRepo": { + "message": "PHP リポジトリを表示" + }, + "rubySDKRepo": { + "message": "Ruby リポジトリを表示" + }, + "goSDKRepo": { + "message": "Go リポジトリを表示" + }, + "createNewClientToManageAsProvider": { + "message": "プロバイダーとして管理するための新しいクライアント組織を作成します。次の請求サイクルに追加のシートが反映されます。" + }, + "selectAPlan": { + "message": "プランを選択" + }, + "thirtyFivePercentDiscount": { + "message": "35%割引" + }, + "monthPerMember": { + "message": "月/メンバーあたり" + }, + "seats": { + "message": "シート" + }, + "addOrganization": { + "message": "組織を追加" + }, + "createdNewClient": { + "message": "新しいクライアントを作成しました" + }, + "noAccess": { + "message": "アクセス不可" + }, + "collectionAdminConsoleManaged": { + "message": "このコレクションは管理コンソールからのみアクセス可能です" + }, + "organizationOptionsMenu": { + "message": "組織メニューの切り替え" + }, + "vaultItemSelect": { + "message": "保管庫のアイテムを選択" + }, + "collectionItemSelect": { + "message": "コレクションのアイテムを選択" + }, + "manageBillingFromProviderPortalMessage": { + "message": "プロバイダーポータルからの請求を管理" } } diff --git a/apps/web/src/locales/ka/messages.json b/apps/web/src/locales/ka/messages.json index d94a701410..104810e936 100644 --- a/apps/web/src/locales/ka/messages.json +++ b/apps/web/src/locales/ka/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "მომხმარებლები ორსაფეხურიანი სისტემაში შესვლის გარეშე ნაპოვნია" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/km/messages.json b/apps/web/src/locales/km/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/km/messages.json +++ b/apps/web/src/locales/km/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/kn/messages.json b/apps/web/src/locales/kn/messages.json index f2a5bdd829..3b34cc7976 100644 --- a/apps/web/src/locales/kn/messages.json +++ b/apps/web/src/locales/kn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "ಅಸುರಕ್ಷಿತ ವೆಬ್‌ಸೈಟ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "unsecuredWebsitesFoundDesc": { - "message": "ಅಸುರಕ್ಷಿತ URI ಗಳೊಂದಿಗೆ ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ವೆಬ್‌ಸೈಟ್ ಅನುಮತಿಸಿದರೆ ನೀವು ಅವರ ಯುಆರ್‌ಐ ಯೋಜನೆಯನ್ನು https: // ಗೆ ಬದಲಾಯಿಸಬೇಕು.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2FA ಇಲ್ಲದೆ ಲಾಗಿನ್‌ಗಳು ಕಂಡುಬಂದಿಲ್ಲ" }, - "inactive2faFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವೆಬ್‌ಸೈಟ್ (ಗಳನ್ನು) ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ, ಅದನ್ನು ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣದೊಂದಿಗೆ ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾಗುವುದಿಲ್ಲ (2fa.directory ಪ್ರಕಾರ). ಈ ಖಾತೆಗಳನ್ನು ಮತ್ತಷ್ಟು ರಕ್ಷಿಸಲು, ನೀವು ಎರಡು ಅಂಶಗಳ ದೃಢೀಕರಣವನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "ಬಹಿರಂಗಪಡಿಸಿದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "exposedPasswordsFoundDesc": { - "message": "ತಿಳಿದಿರುವ ದತ್ತಾಂಶ ಉಲ್ಲಂಘನೆಗಳಲ್ಲಿ ಬಹಿರಂಗಗೊಂಡ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಹೊಂದಿರುವ ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ಹೊಸ ಪಾಸ್‌ವರ್ಡ್ ಬಳಸಲು ನೀವು ಅವುಗಳನ್ನು ಬದಲಾಯಿಸಬೇಕು.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "ದುರ್ಬಲ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "weakPasswordsFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಪ್ರಬಲವಲ್ಲದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳೊಂದಿಗೆ $COUNT$ ವಸ್ತುಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ಬಲವಾದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ಬಳಸಲು ನೀವು ಅವುಗಳನ್ನು ನವೀಕರಿಸಬೇಕು.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "ಮರುಬಳಕೆ ಮಾಡಿದ ಪಾಸ್‌ವರ್ಡ್‌ಗಳು ಕಂಡುಬಂದಿವೆ" }, - "reusedPasswordsFoundDesc": { - "message": "ನಿಮ್ಮ ವಾಲ್ಟ್‌ನಲ್ಲಿ ಮರುಬಳಕೆ ಮಾಡಲಾಗುತ್ತಿರುವ $COUNT$ ಪಾಸ್‌ವರ್ಡ್‌ಗಳನ್ನು ನಾವು ಕಂಡುಕೊಂಡಿದ್ದೇವೆ. ನೀವು ಅವುಗಳನ್ನು ಅನನ್ಯ ಮೌಲ್ಯಕ್ಕೆ ಬದಲಾಯಿಸಬೇಕು.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ko/messages.json b/apps/web/src/locales/ko/messages.json index b54268adbd..76b86a7433 100644 --- a/apps/web/src/locales/ko/messages.json +++ b/apps/web/src/locales/ko/messages.json @@ -573,13 +573,13 @@ "message": "폴더 삭제함" }, "editInfo": { - "message": "Edit info" + "message": "정보 편집" }, "access": { "message": "접근" }, "accessLevel": { - "message": "Access level" + "message": "접근 권한" }, "loggedOut": { "message": "로그아웃됨" @@ -615,7 +615,7 @@ "message": "마스터 비밀번호로 로그인" }, "readingPasskeyLoading": { - "message": "Reading passkey..." + "message": "패스키를 읽는 중..." }, "readingPasskeyLoadingInfo": { "message": "Keep this window open and follow prompts from your browser." @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "안전하지 않은 웹사이트가 발견됨" }, - "unsecuredWebsitesFoundDesc": { - "message": "보관함에 안전하지 않은 URI를 가진 항목 $COUNT$개를 발견했습니다. 웹 사이트에서 허용하는 경우 URI 스키마를 https://로 변경하십시오.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "2단계 인증이 없는 로그인이 발견됨" }, - "inactive2faFoundDesc": { - "message": "보관함에 (2fa.directory에 따른) 2단계 인증이 설정되지 않은 웹 사이트를 $COUNT$개 발견했습니다. 이러한 계정을 더욱 보호하려면 2단계 인증을 사용하십시오.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "노출된 비밀번호가 발견됨" }, - "exposedPasswordsFoundDesc": { - "message": "보관함에 알려진 데이터 유출로 노출된 비밀번호가 있는 $COUNT$개의 항목을 발견했습니다. 새 암호를 사용하도록 암호를 변경해야합니다.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "취약한 비밀번호가 발견됨" }, - "weakPasswordsFoundDesc": { - "message": "강력한 비밀번호가 아닌 $COUNT$개의 항목을 보관함에서 찾았습니다. 더 강력한 암호를 사용하도록 업데이트해야 합니다.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "재사용된 비밀번호가 발견됨" }, - "reusedPasswordsFoundDesc": { - "message": "보관함에서 재사용중인 $COUNT$개의 비밀번호를 찾았습니다. 고유한 값으로 변경해야 합니다.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/lv/messages.json b/apps/web/src/locales/lv/messages.json index d68a62bdcf..cd5991ce7c 100644 --- a/apps/web/src/locales/lv/messages.json +++ b/apps/web/src/locales/lv/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Atrastas nedrošas tīmekļvietnes" }, - "unsecuredWebsitesFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i) ar nedrošām adresēm. Ir ieteicams tās mainīt uz URI ar https://, ja tīmekļvietne to nodrošina.", + "unsecuredWebsitesFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i) ar nedrošām adresēm. Ir ieteicams tās mainīt uz URI ar https://, ja tīmekļvietne to nodrošina.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Atrastie pieteikšanās vienumi bez 2FA" }, - "inactive2faFoundDesc": { - "message": "Glabātavā tika atrasta(s) $COUNT$ tīmekļvietne(s), kurā(s) nav uzstādīta divpakāpju pieteikšanās (vadoties pēc 2fa.directory). Lai labāk aizsargātu šos kontus, ir ieteicams uzstādīt divpakāpju pieteikšanos.", + "inactive2faFoundReportDesc": { + "message": "$VAULT$ tika atrasta(s) $COUNT$ tīmekļvietne(s), kurā(s) nav uzstādīta divpakāpju pieteikšanās (vadoties pēc 2fa.directory). Lai labāk aizsargātu šos kontus, ir ieteicams uzstādīt divpakāpju pieteikšanos.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Atrastas atklātās paroles" }, - "exposedPasswordsFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i), kuros ir paroles, kas ir atklātas zināmās datu noplūdēs. Tos vajadzētu mainīt, lai izmantotu jaunu paroli.", + "exposedPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i), kuros ir paroles, kas ir atklātas zināmās datu noplūdēs. Tos vajadzētu mainīt, lai izmantotu jaunu paroli.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Atrastas vājas paroles" }, - "weakPasswordsFoundDesc": { - "message": "Glabātavā tika atrasts(i) $COUNT$ vienums(i) ar parolēm, kas nav spēcīgas. Tos vajadzētu atjaunināt, lai izmantotu spēcīgākas paroles.", + "weakPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasts(i) $COUNT$ vienums(i) ar parolēm, kas nav spēcīgas. Tos vajadzētu atjaunināt, lai izmantotu spēcīgākas paroles.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Atrastās vairākkārt izmantotās paroles" }, - "reusedPasswordsFoundDesc": { - "message": "Glabātavā tika atrasta(s) $COUNT$ parole(s), kas tiek vairākkārt izmantotas. Ir ieteicams tās nomainīt uz vērtību, kas neatkārtojas citur.", + "reusedPasswordsFoundReportDesc": { + "message": "$VAULT$ tika atrasta(s) $COUNT$ parole(s), kas tiek vairākkārt izmantotas. Ir ieteicams tās nomainīt uz vērtību, kas neatkārtojas citur.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -3360,7 +3380,7 @@ "message": "Ja konts pastāv, tika nosūtīts e-pasta ziņojums ar turpmākām norādēm." }, "deleteRecoverConfirmDesc": { - "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jānospiež zemāk esošā poga, lai apstiprinātu." + "message": "Tika pieprasīts izdzēst Bitwarden kontu. Jāizmanto zemāk esošā poga, lai apstiprinātu." }, "myOrganization": { "message": "Mana apvienība" @@ -3396,7 +3416,7 @@ "message": "Apvienība izdzēsta" }, "organizationDeletedDesc": { - "message": "Apvienība un visi ar to saistītie dati ir izdzēsti." + "message": "Apvienība un visi ar to saistītie dati tika izdzēsti." }, "organizationUpdated": { "message": "Apvienība atjaunināta" @@ -4927,7 +4947,7 @@ "message": "Uziacināt jaunu nodrošinātāja lietotāju, zemāk esošajā laukā ievadot tā Bitwarden konta e-pasta adresi. Ja tam vēl nav Bitwarden konta, tiks vaicāts izveidot jaunu." }, "joinProvider": { - "message": "Pievienot nodrošinātāju" + "message": "Pievienoties nodrošinātāju" }, "joinProviderDesc": { "message": "Tu esi uzaicināts pievienoties augstāk norādītajam nodrošinātājam. Lai to pieņemtu, jāpiesakās vai jāizveido jauns Bitwarden konts." @@ -4945,7 +4965,7 @@ "message": "Nodrošinātājs" }, "newClientOrganization": { - "message": "Jauna sniedzēja apvienība" + "message": "Jauna pasūtītāja apvienība" }, "newClientOrganizationDesc": { "message": "Izveidot jaunu pasūtītāja apvienību, kas būs piesaistīta šim kontam kā nodrošinātājam. Tas sniegs iespēju piekļūt šai apvienībai un to pārvaldīt." @@ -5661,7 +5681,7 @@ "message": "Rēķinu vēsture" }, "backToReports": { - "message": "Atgriezties pie atskaitēm" + "message": "Atgriezties pie pārskatiem" }, "organizationPicker": { "message": "Apvienību atlasītājs" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Piešķirt piekļuvi krājumiem, pievienojot tos šai grupai." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Var piešķirt tikai pārvaldītos krājumus." + }, "accessAllCollectionsDesc": { "message": "Piešķirt piekļuvi visiem pašreizējiem un turpmākajiem krājumiem." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Nodrošinātāju portāls" }, + "success": { + "message": "Izdevās" + }, "viewCollection": { "message": "Skatīt krājumu" }, @@ -7738,42 +7764,42 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Mašīnu kontus nav iespējams izveidot apturētās apvienībās. Lūgums vērsties pie savas apvienības īpašnieka pēc palīdzības." }, "machineAccount": { - "message": "Machine account", + "message": "Mašīnas konts", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Mašīnu konti", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Jauns mašīnas konts", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Jāizveido jauns mašīnas konts, lai sāktu automatizēt piekļuvi noslēpumiem.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Vēl nav nekā, ko parādīt", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Izdzēst mašīnu kontus", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Izdzēst mašīnas kontu", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Skatīt mašīnas kontu", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Mašīnas konta $MACHINE_ACCOUNT$ izdzēšana ir paliekoša un neatgriezeniska.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7808,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Mašīnu kontu izdzēšana ir paliekoša un neatgriezeniska." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Izdzēst $COUNT$ mašīnu kontu(s)", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7820,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Mašīnas konts ir izdzēsts" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Mašīnu konti ir izdzēsti" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Meklēt mašīnu kontus", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Labot mašīnas kontu", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Mašīnas konta nosaukums", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Mašīnas konts ir izveidots", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Mašīnas konts ir atjaunināts", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Piešķirt mašīnau kontiem piekļuvi šim projektam." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Ievadīt vai atlasīt mašīnu kontus" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Jāpievieno mašīnu konti, lai piešķirtu piekļuvi" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Piešķirt kopām vai cilvēkiem piekļuvi šim mašīnas kontam." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Piešķirt projektus šim mašīnas kontam. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Izveidot mašīnas kontu" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Cilvēku noņemšana no mašīnas konta nenoņem to izveidotās piekļuves pilnvaras. Labākajai drošības pieejai ir ieteicams atsaukt piekļuves pilnvaras, kuras ir izveidojuši cilvēki, kuri ir noņemti no mašīnas konta." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Noņemt piekļuvi šim mašīnas kontam" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Šī darbība noņems piekļuvi mašīnas kontam." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Iekļauti $COUNT$ mašīnu konti", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7882,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mēnesī par papildu mašīnu kontiem", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7891,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Papildu mašīnu konti" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Plānā ir iekļauti $COUNT$ mašīnu konti.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7903,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Papildu mašīnu kontus var pievienot par $COST$ mēnesī.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7912,168 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Ierobežot mašīnu kontus (izvēles)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Uzstāda mašīnu kontu skaita ierobežojumu. Tiklīdz tas ir sasniegts, nebūs iespējams izveidot jaunus mašīnu kontus." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Mašīnu kontu skaita ierobežojums (izvēles)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Lielākās iespējamās mašīnas konta izmaksas" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Mašīnas konta piekļuve ir atjaunināta" }, - "unassignedItemsBanner": { - "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un ir sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "restrictedGroupAccessDesc": { + "message": "Sevi nevar pievienot kopai." }, "unassignedItemsBannerSelfHost": { - "message": "Jāņem vērā: 2024. gada 2. maijā nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + "message": "Jāņem vērā: no 2024. gada 2. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs sasniedzami tikai no pārvaldības konsoles, kur šie vienumi jāpiešķir krājumam, lai padarītu tos redzamus." + }, + "unassignedItemsBannerNotice": { + "message": "Jāņem vērā: nepiešķirti apvienības vienumi vairs nav redzami skatā \"Visas glabātavas\" dažādās ierīcēs un tagad ir pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Jāņem vērā: no 2024. gada 16. maija nepiešķirti apvienības vienumi vairs nebūs redzami skatā \"Visas glabātavas\" dažādās ierīcēs un būs pieejami tikai pārvaldības konsolē." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Piešķirt šos vienumus krājumam", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", lai padarītu tos redzamus.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Izdzēst nodrošinātāju" + }, + "deleteProviderConfirmation": { + "message": "Nodrošinātāja izdzēšana ir paliekoša un neatgriezeniska. Jāievada sava galvenā parole, lai apliecinātu nodrošinātāja un visu saistīto datu izdzēšanu." + }, + "deleteProviderName": { + "message": "Nevar izdzēst $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Nodrošinātājs izdzēsts" + }, + "providerDeletedDesc": { + "message": "Nodrošinātājs un visi ar to saistītie dati tika izdzēsti." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Tika pieprasīts izdzēst šo nodrošinātāju. Jāizmanto zemāk esošā poga, lai apstiprinātu." + }, + "deleteProviderWarning": { + "message": "Nodrošinātāja izdzēšana ir paliekoša. To nevar atsaukt." + }, + "errorAssigningTargetCollection": { + "message": "Kļūda mērķa krājuma piešķiršanā." + }, + "errorAssigningTargetFolder": { + "message": "Kļūda mērķa mapes piešķiršanā." + }, + "integrationsAndSdks": { + "message": "Integrācijas un izstrādātāju rīkkopas", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrācijas" + }, + "integrationsDesc": { + "message": "Automātiski sinhronizēt noslēpumus no Bitwarden Noslēpumu pārvaldnieka uz trešās puses pakalpojumu." + }, + "sdks": { + "message": "Izstrādātāju rīkkopas" + }, + "sdksDesc": { + "message": "Bitwarden Noslēpumu pārvaldnieka izstrādātāju rīkkopa ir izmantojama ar zemāk esošajām programmēšanas valodām, lai veidotu pats savas lietotnes." + }, + "setUpGithubActions": { + "message": "Iestatīt GitHub darbības" + }, + "setUpGitlabCICD": { + "message": "Iestatīt GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Iestatīt Ansible" + }, + "cSharpSDKRepo": { + "message": "Skatīt C# glabātavu" + }, + "cPlusPlusSDKRepo": { + "message": "Skatīt C++ glabātavu" + }, + "jsWebAssemblySDKRepo": { + "message": "Skatīt JS WebAssembly glabātavu" + }, + "javaSDKRepo": { + "message": "Skatīt Java glabātavu" + }, + "pythonSDKRepo": { + "message": "Skatīt Python glabātavu" + }, + "phpSDKRepo": { + "message": "Skatīt PHP glabātavu" + }, + "rubySDKRepo": { + "message": "Skatīt Ruby glabātavu" + }, + "goSDKRepo": { + "message": "Skatīt Go glabātavu" + }, + "createNewClientToManageAsProvider": { + "message": "Izveidot jaunu klienta apvienību, ko pārvaldīt kā nodrošinātājam. Papildu vietas tiks atspoguļotas nākamajā norēķinu posmā." + }, + "selectAPlan": { + "message": "Atlasīt plānu" + }, + "thirtyFivePercentDiscount": { + "message": "35% atlaide" + }, + "monthPerMember": { + "message": "mēnesī par dalībnieku" + }, + "seats": { + "message": "Vietas" + }, + "addOrganization": { + "message": "Pievienot apvienību" + }, + "createdNewClient": { + "message": "Veiksmīgi izveidots jauns klients" + }, + "noAccess": { + "message": "Nav piekļuves" + }, + "collectionAdminConsoleManaged": { + "message": "Šis krājums ir pieejams tikai pārvaldības konsolē" + }, + "organizationOptionsMenu": { + "message": "Pārslēgt apvienību izvēlni" + }, + "vaultItemSelect": { + "message": "Atlasīt glabātavas vienumu" + }, + "collectionItemSelect": { + "message": "Atlasīt krājuma vienumu" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Norēķinus var pārvaldīt Nodrošinātāju portālā" } } diff --git a/apps/web/src/locales/ml/messages.json b/apps/web/src/locales/ml/messages.json index 4f81532796..3bd5ca3049 100644 --- a/apps/web/src/locales/ml/messages.json +++ b/apps/web/src/locales/ml/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured Websites Found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins Without 2FA Found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed Passwords Found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "ദുർബലമായ പാസ്‌വേഡുകൾ കണ്ടെത്തി" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused Passwords Found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/mr/messages.json b/apps/web/src/locales/mr/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/mr/messages.json +++ b/apps/web/src/locales/mr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/my/messages.json b/apps/web/src/locales/my/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/my/messages.json +++ b/apps/web/src/locales/my/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nb/messages.json b/apps/web/src/locales/nb/messages.json index 0719b129b3..5b544ff3a4 100644 --- a/apps/web/src/locales/nb/messages.json +++ b/apps/web/src/locales/nb/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Usikrede nettsteder ble funnet" }, - "unsecuredWebsitesFoundDesc": { - "message": "Vi fant $COUNT$ objekter i hvelvet ditt som benytter usikrede URI-er. Du burde endre deres URI-er til å benytte https://, dersom det nettstedet tillater det.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Pålogginger som støtter 2FA ble funnet" }, - "inactive2faFoundDesc": { - "message": "Vi fant $COUNT$ nettsted(er) i hvelvet ditt som kanskje ikke har blitt satt opp med 2-trinnspålogging (i følge 2fa.directory). For å beskytte disse kontoene ytterligere, burde du sette opp 2-trinnspålogging.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Eksponerte passord ble funnet" }, - "exposedPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ elementer i hvelvet ditt med passord eksponert i kjente datainnbrudd. Du bør endre passordet på dem.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svake passord ble funnet" }, - "weakPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ objekter i hvelvet ditt som har passord som ikke er sterke. Du burde oppdatere dem til å bruke sterkere passord.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Gjenbrukte passord ble funnet" }, - "reusedPasswordsFoundDesc": { - "message": "Vi fant $COUNT$ passord som blir gjenbrukt i hvelvet ditt. Du burde endre dem slik at de er unike.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Gi tilgang til samlinger ved å legge dem til i denne gruppen." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Gi tilgang til alle nåværende og fremtidige samlinger." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ne/messages.json b/apps/web/src/locales/ne/messages.json index e0ca2b4f0a..24aaf3d0d3 100644 --- a/apps/web/src/locales/ne/messages.json +++ b/apps/web/src/locales/ne/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nl/messages.json b/apps/web/src/locales/nl/messages.json index 3ab66e763b..5a203b5a51 100644 --- a/apps/web/src/locales/nl/messages.json +++ b/apps/web/src/locales/nl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Onveilige websites gevonden" }, - "unsecuredWebsitesFoundDesc": { - "message": "We hebben $COUNT$ items met onbeveiligde URIs in je kluis gevonden. Als de website het ondersteunt, moet je de URI naar https:// wijzigen.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We vonden $COUNT$ items met onbeveiligde URIs in $VAULT$. Als de website het ondersteunt, kun je de URI beter aanpassen naar https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins zonder 2FA gevonden" }, - "inactive2faFoundDesc": { - "message": "We hebben $COUNT$ website(s) in je kluis gevonden waar je (volgens 2fa.directory) nog tweestapsaanmelding kunt instellen. Om deze accounts beter te beschermen, zou je tweestapsaanmelding moeten inschakelen.", + "inactive2faFoundReportDesc": { + "message": "We vonden $COUNT$ website(s) in $VAULT$ waar je tweestapsaanmelding nog niet gebruikt (terwijl dat volgens 2fa.directory wel kan). Om deze accounts beter te beschermen, kun je tweestapsaanmelding instellen.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Gelekte wachtwoorden gevonden" }, - "exposedPasswordsFoundDesc": { - "message": "We hebben in je kluis $COUNT$ wachtwoorden gevonden die zijn gelekt. Je zou voor deze accounts een nieuw wachtwoord moeten instellen.", + "exposedPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ items in $VAULT$ met wachtwoorden die voorkomen in bekende datalekken. Je kunt die wachtwoorden het beste vervangen door een nieuw wachtwoord.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zwakke wachtwoorden gevonden" }, - "weakPasswordsFoundDesc": { - "message": "We hebben $COUNT$ zwakke wachtwoorden in je kluis gevonden. Je zou ze moeten veranderen in sterke wachtwoorden.", + "weakPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ items in $VAULT$ met zwakke wachtwoorden. Je kunt die wachtwoorden het beste vervangen door sterke wachtwoorden.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Dubbele wachtwoorden gevonden" }, - "reusedPasswordsFoundDesc": { - "message": "We hebben $COUNT$ dubbele wachtwoorden in je kluis gevonden. Je zou deze moeten veranderen in unieke wachtwoorden.", + "reusedPasswordsFoundReportDesc": { + "message": "We vonden $COUNT$ wachtwoorden die vaker voorkomen in $VAULT$. Je kunt die wachtwoorden het beste vervangen door een uniek wachtwoord.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Geef toegang tot collecties door ze aan deze groep toe te voegen." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Geef toegang tot alle huidige en toekomstige collecties." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Providerportaal" }, + "success": { + "message": "Succes" + }, "viewCollection": { "message": "Collectie weergeven" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Toegang tot machine-account bijgewerkt" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Je kunt jezelf niet aan een groep toevoegen." }, "unassignedItemsBannerSelfHost": { "message": "Kennisgeving: Vanaf 2 mei 2024 zijn niet-toegewezen organisatie-items op geen enkel apparaat meer zichtbaar in de weergave van alle kluisjes en alleen toegankelijk via de Admin Console. Je kunt deze items in het Admin Console aan een collectie toewijzen om ze zichtbaar te maken." + }, + "unassignedItemsBannerNotice": { + "message": "Let op: Niet-toegewezen organisatie-items zijn niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en zijn nu alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Let op: Vanaf 16 mei 2024 zijn niet-toegewezen organisatie-items niet langer zichtbaar in de weergave van alle kluissen op verschillende apparaten en alleen toegankelijk via de Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Deze items toewijzen aan een collectie van de", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "om ze zichtbaar te maken.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Provider verwijderen" + }, + "deleteProviderConfirmation": { + "message": "Het verwijderen van een provider is definitief en onomkeerbaar. Voer je hoofdwachtwoord in om het verwijderen van de provider en alle bijbehorende gegevens te bevestigen." + }, + "deleteProviderName": { + "message": "Kan $ID$ niet verwijderen", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Je moet alle clients ontkoppelen voordat je $ID$ kunt verwijderen.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider verwijderd" + }, + "providerDeletedDesc": { + "message": "De organisatie en alle bijhorende gegevens zijn verwijderd." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Je hebt het verwijderen van deze provider aangevraagd. Gebruik deonderstaande knop om te bevestigen." + }, + "deleteProviderWarning": { + "message": "Het verwijderen van een provider is definitief. Je kunt het niet ongedaan maken." + }, + "errorAssigningTargetCollection": { + "message": "Fout bij toewijzen doelverzameling." + }, + "errorAssigningTargetFolder": { + "message": "Fout bij toewijzen doelmap." + }, + "integrationsAndSdks": { + "message": "Integraties & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integraties" + }, + "integrationsDesc": { + "message": "Automatisch secrets synchroniseren van Bitwarden Secrets Manager met een dienst van derden." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Gebruik Bitwarden Secrets Manager SDK in de volgende programmeertalen om je eigen applicaties te bouwen." + }, + "setUpGithubActions": { + "message": "GitHub Actions instellen" + }, + "setUpGitlabCICD": { + "message": "GitLab CI/CD instellen" + }, + "setUpAnsible": { + "message": "Ansibel instellen" + }, + "cSharpSDKRepo": { + "message": "C#-repository bekijken" + }, + "cPlusPlusSDKRepo": { + "message": "C++-repository bekijken" + }, + "jsWebAssemblySDKRepo": { + "message": "JS WebAssembly-repository bekijken" + }, + "javaSDKRepo": { + "message": "Java-repository bekijken" + }, + "pythonSDKRepo": { + "message": "Python-repository bekijken" + }, + "phpSDKRepo": { + "message": "Php-repository bekijken" + }, + "rubySDKRepo": { + "message": "Ruby-repository bekijken" + }, + "goSDKRepo": { + "message": "Go-repository bekijken" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "Geen toegang" + }, + "collectionAdminConsoleManaged": { + "message": "Deze collectie is alleen toegankelijk vanaf de admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/nn/messages.json b/apps/web/src/locales/nn/messages.json index a80816b511..7569f5a4ee 100644 --- a/apps/web/src/locales/nn/messages.json +++ b/apps/web/src/locales/nn/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Gjenbrukte passord blei funne" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/or/messages.json b/apps/web/src/locales/or/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/or/messages.json +++ b/apps/web/src/locales/or/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/pl/messages.json b/apps/web/src/locales/pl/messages.json index 9532e8d25c..e1a48f0fb0 100644 --- a/apps/web/src/locales/pl/messages.json +++ b/apps/web/src/locales/pl/messages.json @@ -296,7 +296,7 @@ "message": "Szukaj w organizacji" }, "searchMembers": { - "message": "Szukaj w użytkownikach" + "message": "Szukaj członków" }, "searchGroups": { "message": "Szukaj w grupach" @@ -854,7 +854,7 @@ "message": "Brak użytkowników do wyświetlenia." }, "noMembersInList": { - "message": "Brak użytkowników do wyświetlenia." + "message": "Brak członków do wyświetlenia." }, "noEventsInList": { "message": "Brak wydarzeń do wyświetlenia." @@ -1534,7 +1534,7 @@ "message": "Włącz dwustopniowe logowanie dla swojej organizacji." }, "twoStepLoginEnterpriseDescStart": { - "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla użytkowników za pomocą ", + "message": "Wymuszaj opcje logowania dwustopniowego Bitwarden dla członków za pomocą ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Enforce Bitwarden Two-step Login options for members by using the Two-step Login Policy.'" }, "twoStepLoginPolicy": { @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Znaleźliśmy niezabezpieczone strony" }, - "unsecuredWebsitesFoundDesc": { - "message": "Znaleźliśmy elementy w Twoim sejfie zawierające niezabezpieczone adresy URI. Jeśli witryna to umożliwia, zmień schemat adresu na protokół HTTPS.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Znaleźliśmy $COUNT$ elementów w Twoim $VAULT$, zawierających niezabezpieczone identyfikatory URI. Jeśli witryna to umożliwia, należy zmienić schemat identyfikatora URI na https://.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Znaleźliśmy elementy bez włączonej opcji logowania dwustopniowego" }, - "inactive2faFoundDesc": { - "message": "Znaleźliśmy $COUNT$ witryn(y) w sejfie, które mogą nie być skonfigurowane z wykorzystaniem logowania dwustopniowego (wg 2fa.directory). Aby dodatkowo zabezpieczyć te konta, należy skonfigurować logowanie dwustopniowe.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Znaleźliśmy ujawnione hasła" }, - "exposedPasswordsFoundDesc": { - "message": "Znaleźliśmy elementy w sejfie, które zawierają ujawnione hasła w znanych wyciekach danych. Zmień te hasła.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Znaleźliśmy słabe hasła" }, - "weakPasswordsFoundDesc": { - "message": "Znaleźliśmy elementy w sejfie, które zawierają słabe hasła. Zaktualizuj je na silniejsze hasła.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Znaleźliśmy identyczne hasła" }, - "reusedPasswordsFoundDesc": { - "message": "Znaleźliśmy hasła, które powtarzają się w sejfie. Zmień je, aby były unikalne.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2085,7 +2105,7 @@ "message": "Konto Premium" }, "premiumAccessDesc": { - "message": "Możesz przyznać dostęp premium wszystkim użytkownikom w Twojej organizacji za $PRICE$ /$INTERVAL$.", + "message": "Możesz przyznać dostęp premium wszystkim członkom Twojej organizacji za $PRICE$ /$INTERVAL$.", "placeholders": { "price": { "content": "$1", @@ -2541,7 +2561,7 @@ } }, "trialSecretsManagerThankYou": { - "message": "Thanks for signing up for Bitwarden Secrets Manager for $PLAN$!", + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden w planie $PLAN$!", "placeholders": { "plan": { "content": "$1", @@ -2691,7 +2711,7 @@ "message": "Czy na pewno chcesz usunąć tę kolekcję?" }, "editMember": { - "message": "Edytuj użytkownika" + "message": "Edytuj członka" }, "fieldOnTabRequiresAttention": { "message": "Pole na karcie '$TAB$' wymaga uwagi.", @@ -2751,7 +2771,7 @@ "message": "Administrator" }, "adminDesc": { - "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i użytkowników w Twojej organizacji." + "message": "Administratorzy mają dostęp do wszystkich elementów, kolekcji i członków Twojej organizacji" }, "user": { "message": "Użytkownik" @@ -3234,7 +3254,7 @@ "message": "Dostęp grupowy" }, "groupAccessUserDesc": { - "message": "Zmień grupy, do których należy użytkownik." + "message": "Przyznaj członkowi dostęp do kolekcji, dodając go do 1 lub więcej grup." }, "invitedUsers": { "message": "Użytkownicy zostali zaproszeni" @@ -3270,10 +3290,10 @@ } }, "confirmUsers": { - "message": "Zatwierdź użytkowników" + "message": "Zatwierdź członków" }, "usersNeedConfirmed": { - "message": "Posiadasz użytkowników, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Użytkownicy nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." + "message": "Posiadasz członków, którzy zaakceptowali zaproszenie, ale muszą jeszcze zostać potwierdzeni. Członkowie nie będą posiadali dostępu do organizacji, dopóki nie zostaną potwierdzeni." }, "startDate": { "message": "Data rozpoczęcia" @@ -3486,10 +3506,10 @@ "message": "Wpisz identyfikator instalacji" }, "limitSubscriptionDesc": { - "message": "Ustaw limit liczby stanowisk subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "limitSmSubscriptionDesc": { - "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych użytkowników." + "message": "Ustaw limit liczby subskrypcji menedżera sekretów. Po osiągnięciu tego limitu nie będziesz mógł zapraszać nowych członków." }, "maxSeatLimit": { "message": "Maksymalna liczba stanowisk (opcjonalnie)", @@ -3510,7 +3530,7 @@ "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." }, "subscriptionUserSeats": { - "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ użytkowników.", + "message": "Twoja subskrypcja pozwala na łączną liczbę $COUNT$ członków.", "placeholders": { "count": { "content": "$1", @@ -3537,10 +3557,10 @@ "message": "Aby uzyskać dodatkową pomoc w zarządzaniu subskrypcją, skontaktuj się z działem obsługi klienta." }, "subscriptionUserSeatsUnlimitedAutoscale": { - "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę stanowisk w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych użytkowników." + "message": "Zmiany w subskrypcji spowodują proporcjonalne zmiany w rozliczeniach. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc w subskrypcji, otrzymasz proporcjonalną opłatę za dodatkowych członków." }, "subscriptionUserSeatsLimitedAutoscale": { - "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni użytkownicy przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych użytkowników, aż do osiągnięcia limitu miejsc $MAX$.", + "message": "Korekty subskrypcji spowodują proporcjonalne zmiany w sumach rozliczeniowych. Jeśli nowo zaproszeni członkowie przekroczą liczbę miejsc objętych subskrypcją, natychmiast otrzymasz proporcjonalną opłatę za dodatkowych członków, aż do osiągnięcia limitu miejsc $MAX$.", "placeholders": { "max": { "content": "$1", @@ -4053,7 +4073,7 @@ "message": "Wszystkie funkcje zespołów oraz:" }, "includeAllTeamsStarterFeatures": { - "message": "All Teams Starter features, plus:" + "message": "Wszystkie funkcje z Teams Starter, plus:" }, "chooseMonthlyOrAnnualBilling": { "message": "Wybierz miesięczną lub roczną płatność" @@ -4083,7 +4103,7 @@ "message": "Identyfikator SSO" }, "ssoIdentifierHintPartOne": { - "message": "Podaj ten identyfikator swoim użytkownikom, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", + "message": "Podaj ten identyfikator swoim członkowm, aby zalogować się za pomocą SSO. Aby pominąć ten krok, ustaw ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -5210,7 +5230,7 @@ "message": "Zweryfikuj certyfikaty" }, "spUniqueEntityId": { - "message": "Set a unique SP entity ID" + "message": "Ustaw unikalny identyfikator jednostki SP" }, "spUniqueEntityIdDesc": { "message": "Wygeneruj identyfikator unikalny dla Twojej organizacji" @@ -5426,13 +5446,13 @@ "message": "Opcje odszyfrowania użytkownika" }, "memberDecryptionPassDesc": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrują dane sejfu za pomocą hasła głównego." + "message": "Po uwierzytelnieniu członkowie odszyfrują dane sejfu za pomocą hasła głównego." }, "keyConnector": { "message": "Key Connector" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, użytkownicy nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", + "message": "Połącz logowanie za pomocą SSO z Twoim serwerem kluczy odszyfrowania. Używając tej opcji, członkowie nie będą musieli używać swoich haseł głównych, aby odszyfrować dane sejfu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -6455,11 +6475,14 @@ "message": "Informacje o grupie" }, "editGroupMembersDesc": { - "message": "Przyznaj użytkownikom dostęp do kolekcji przypisanych do grupy." + "message": "Przyznaj członkom dostęp do kolekcji przypisanych do grupy." }, "editGroupCollectionsDesc": { "message": "Przyznaj dostęp do kolekcji, dodając je do tej grupy." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Udziel dostępu do wszystkich bieżących i przyszłych kolekcji." }, @@ -6467,7 +6490,7 @@ "message": "Jeśli zaznaczone, zastąpi to wszystkie inne uprawnienia kolekcji." }, "selectMembers": { - "message": "Wybierz użytkowników" + "message": "Wybierz członków" }, "selectCollections": { "message": "Wybierz kolekcje" @@ -6476,7 +6499,7 @@ "message": "Rola" }, "removeMember": { - "message": "Usuń użytkownika" + "message": "Usuń członka" }, "collection": { "message": "Kolekcja" @@ -6500,7 +6523,7 @@ "message": "Nie dodano kolekcji" }, "noMembersAdded": { - "message": "Nie dodano użytkowników" + "message": "Nie dodano członków" }, "noGroupsAdded": { "message": "Nie dodano grup" @@ -6680,13 +6703,13 @@ "message": "Wysłać kod ponownie" }, "memberColumnHeader": { - "message": "Member" + "message": "Członek" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupa/Członek" }, "selectGroupsAndMembers": { - "message": "Wybierz grupy i użytkowników" + "message": "Wybierz grupy i członków" }, "selectGroups": { "message": "Wybierz grupy" @@ -6695,22 +6718,22 @@ "message": "Uprawnienia ustawione dla użytkownika zastąpią uprawnienia ustawione przez grupę tego użytkownika" }, "noMembersOrGroupsAdded": { - "message": "Nie dodano użytkowników lub grup" + "message": "Nie dodano członków lub grup" }, "deleted": { "message": "Usunięto" }, "memberStatusFilter": { - "message": "Filtr statusu użytkownika" + "message": "Filtr statusu członka" }, "inviteMember": { - "message": "Zaproś użytkownika" + "message": "Zaproś członka" }, "needsConfirmation": { "message": "Wymaga potwierdzenia" }, "memberRole": { - "message": "Rola użytkownika" + "message": "Rola członka" }, "moreFromBitwarden": { "message": "Więcej od Bitwarden" @@ -6737,7 +6760,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Przejdź na wyższy plan, aby zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", @@ -6746,7 +6769,7 @@ } }, "teamsStarterPlanInvLimitReachedNoManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade your plan and invite more members.", + "message": "Plan Teams Starter może mieć maksymalnie $SEATCOUNT$ członków. Skontaktuj się z właścicielem organizacji, aby przejść na wyższy plan i zaprosić więcej członków.", "placeholders": { "seatcount": { "content": "$1", @@ -7105,7 +7128,7 @@ "message": "Zaufane urządzenia" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Po uwierzytelnieniu użytkownicy odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", + "message": "Po uwierzytelnieniu członkowie odszyfrowają dane sejfu przy użyciu klucza zapisanego na ich urządzeniu.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { @@ -7229,7 +7252,7 @@ "message": "Brak hasła głównego" }, "removeMembersWithoutMasterPasswordWarning": { - "message": "Usuwanie użytkowników, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." + "message": "Usuwanie członków, którzy nie mają hasła głównego bez ustawienia go, może ograniczyć dostęp do ich pełnych kont." }, "approvedAuthRequest": { "message": "Zatwierdzone urządzenie dla $ID$.", @@ -7262,7 +7285,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Rozpocznij 7-dniowy darmowy okres próbny Menedżera Sekretów Bitwarden dla $ORG$", "placeholders": { "org": { "content": "$1", @@ -7508,7 +7531,7 @@ "message": "Przyznaj grupom lub członkom dostęp do tej kolekcji." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Przyznaj członkom dostęp do tej kolekcji." }, "adminCollectionAccess": { "message": "Administratorzy mogą mieć dostęp do kolekcji i zarządzać nimi." @@ -7548,7 +7571,7 @@ "message": "Szczegóły potwierdzenia" }, "smFreeTrialThankYou": { - "message": "Thank you for signing up for Bitwarden Secrets Manager!" + "message": "Dziękujemy za zarejestrowanie się do Menedżera Sekretów Bitwarden!" }, "smFreeTrialConfirmationEmail": { "message": "Wysłaliśmy e-mail weryfikacyjny na adres " @@ -7557,7 +7580,7 @@ "message": "Ta akcja jest nieodwracalna" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Włączenie tej funkcji spowoduje deprekację roli menedżera i zastąpi ją uprawnieniami do zarządzania uprawnieniami. To zajmie kilka chwil. Nie wprowadzaj żadnych zmian w organizacji dopóki nie zostanie zakończone. Czy na pewno chcesz kontynuować?" }, "sorryToSeeYouGo": { "message": "Przykro nam, że nas opuszczasz! Pomóż ulepszyć Bitwarden udostępniając informacje dlaczego anulujesz.", @@ -7598,7 +7621,7 @@ "message": "Witaj w nowej i ulepszonej aplikacji internetowej. Dowiedz się więcej o tym, co się zmieniło." }, "releaseBlog": { - "message": "Read release blog" + "message": "Przeczytaj blog z informacjami o wydaniu" }, "adminConsole": { "message": "Konsola administratora" @@ -7606,14 +7629,17 @@ "providerPortal": { "message": "Portal dostawcy" }, + "success": { + "message": "Sukces" + }, "viewCollection": { "message": "Zobacz kolekcję" }, "restrictedGroupAccess": { - "message": "You cannot add yourself to groups." + "message": "Nie możesz dodać siebie do grup." }, "restrictedCollectionAccess": { - "message": "You cannot add yourself to collections." + "message": "Nie możesz dodać siebie do kolekcji." }, "assign": { "message": "Przypisz" @@ -7738,42 +7764,42 @@ "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Konta dla maszyn nie mogą być tworzone w zawieszonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, "machineAccount": { - "message": "Machine account", + "message": "Konto dla maszyny", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Konta dla maszyn", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nowe konto dla maszyny", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Utwórz nowe konto dla maszyny, aby rozpocząć automatyzację dostępu do sekretu.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Nie ma nic do pokazania", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Usuń konta maszyn", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Usuń konto maszyny", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Zobacz konto maszyny", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "Usuwanie konta maszyny $MACHINE_ACCOUNT$ jest trwałe i nieodwracalne.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7808,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Usuwanie kont maszyn jest trwałe i nieodwracalne." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Usuń $COUNT$ kont maszyn", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7820,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Konto maszyny usunięte" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Konta maszyn usunięte" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Szukaj kont maszyn", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Edytuj konto maszyny", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nazwa konta maszyny", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Konto maszyny utworzone", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Konto maszyny zaktualizowane", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Przyznaj dostęp kontom maszynowym do tego projektu." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Wpisz lub wybierz konta maszyn" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Dodaj konta maszyn, aby udzielić dostępu" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Przyznaj grupom lub ludziom dostęp do tego konta maszyny." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Przypisz projekty do tego konta maszyny. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Utwórz konto dla maszyny" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Usuwanie osób z konta maszyny nie usuwa tokenów dostępu. Dla zapewnienia najlepszej praktyki bezpieczeństwa zaleca się cofnięcie tokenów dostępu stworzonych przez osoby usunięte z konta maszyny." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Usuń dostęp do tego konta maszyny" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Ta akcja usunie Twój dostęp do konta maszyny." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "Uwzględnione konta maszyn: $COUNT$", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7882,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ miesięcznie dla dodatkowych kont dla maszyn", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7891,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Dodatkowe konta dla maszyn" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Twój plan obejmuje $COUNT$ kont dla maszyn.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7903,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Możesz dodać dodatkowe konta dla maszyn za $COST$ miesięcznie.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7912,168 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limit kont dla maszyn (opcjonalnie)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Ustaw limit liczby kont dla maszyn. Po osiągnięciu tego limitu nie będziesz mógł tworzyć nowych kont." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limit konta dla maszyn (opcjonalnie)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Maksymalny potencjalny koszt kont dla maszyn" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Zaktualizowano dostęp do konta dla maszyny" }, - "unassignedItemsBanner": { - "message": "Uwaga: Nieprzypisane elementy w organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + "restrictedGroupAccessDesc": { + "message": "Nie możesz dodać siebie do grupy." }, "unassignedItemsBannerSelfHost": { "message": "Uwaga: 2 maja 2024 r. nieprzypisane elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną. Przypisz te elementy do kolekcji z konsoli administracyjnej, aby były one widoczne." + }, + "unassignedItemsBannerNotice": { + "message": "Uwaga: Nieprzypisane elementy organizacji nie są już widoczne w widoku Wszystkie sejfy na urządzeniach i są teraz dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Uwaga: 16 maja 2024 r. nieprzypisana elementy w organizacji nie będą już widoczne w widoku Wszystkie sejfy na urządzeniach i będą dostępne tylko przez Konsolę Administracyjną." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Przypisz te elementy do kolekcji z", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby były widoczne.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Usuń dostawcę" + }, + "deleteProviderConfirmation": { + "message": "Usunięcie dostawcy jest trwałe i nieodwracalne. Wprowadź swoje hasło główne, aby potwierdzić usunięcie dostawcy i wszystkich powiązanych danych." + }, + "deleteProviderName": { + "message": "Nie można usunąć $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Musisz odłączyć wszystkich klientów zanim będziesz mógł usunąć $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Dostawca został usunięty" + }, + "providerDeletedDesc": { + "message": "Dostawca i wszystkie powiązane dane zostały usunięte." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Poprosiłeś o usunięcie tego dostawcy. Kliknij poniższy przycisk, aby to potwierdzić." + }, + "deleteProviderWarning": { + "message": "Usunięcie dostawcy jest nieodwracalne. Ta czynność nie może zostać cofnięta." + }, + "errorAssigningTargetCollection": { + "message": "Wystąpił błąd podczas przypisywania kolekcji." + }, + "errorAssigningTargetFolder": { + "message": "Wystąpił błąd podczas przypisywania folderu." + }, + "integrationsAndSdks": { + "message": "Integracje i SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integracje" + }, + "integrationsDesc": { + "message": "Automatycznie synchronizuj sekrety z Menedżera Sekretnych Bitwarden do usługi innej firmy." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Użyj SDK Menedżera Sekretów Bitwarden w następujących językach programowania, aby zbudować własne aplikacje." + }, + "setUpGithubActions": { + "message": "Skonfiguruj Github Actions" + }, + "setUpGitlabCICD": { + "message": "Skonfiguruj GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Skonfiguruj Ansible" + }, + "cSharpSDKRepo": { + "message": "Zobacz repozytorium C#" + }, + "cPlusPlusSDKRepo": { + "message": "Zobacz repozytorium C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Zobacz repozytorium JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Zobacz repozytorium Java" + }, + "pythonSDKRepo": { + "message": "Zobacz repozytorium Pythona" + }, + "phpSDKRepo": { + "message": "Zobacz repozytorium php" + }, + "rubySDKRepo": { + "message": "Zobacz repozytorium Ruby" + }, + "goSDKRepo": { + "message": "Zobacz repozytorium Go" + }, + "createNewClientToManageAsProvider": { + "message": "Utwórz nową organizację klienta do zarządzania jako dostawca. Dodatkowe miejsca zostaną odzwierciedlone w następnym cyklu rozliczeniowym." + }, + "selectAPlan": { + "message": "Wybierz plan" + }, + "thirtyFivePercentDiscount": { + "message": "Zniżka 35%" + }, + "monthPerMember": { + "message": "miesięcznie za członka" + }, + "seats": { + "message": "Miejsca" + }, + "addOrganization": { + "message": "Dodaj organizację" + }, + "createdNewClient": { + "message": "Pomyślnie utworzono nowego klienta" + }, + "noAccess": { + "message": "Brak dostępu" + }, + "collectionAdminConsoleManaged": { + "message": "Ta kolekcja jest dostępna tylko z konsoli administracyjnej" + }, + "organizationOptionsMenu": { + "message": "Przełącz menu organizacji" + }, + "vaultItemSelect": { + "message": "Wybierz element sejfu" + }, + "collectionItemSelect": { + "message": "Wybierz element kolekcji" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Zarządzaj płatnościami z portalu dostawcy" } } diff --git a/apps/web/src/locales/pt_BR/messages.json b/apps/web/src/locales/pt_BR/messages.json index 084d4c95d9..fc3c4b1a51 100644 --- a/apps/web/src/locales/pt_BR/messages.json +++ b/apps/web/src/locales/pt_BR/messages.json @@ -579,7 +579,7 @@ "message": "Acesso" }, "accessLevel": { - "message": "Access level" + "message": "Nível de acesso" }, "loggedOut": { "message": "Sessão encerrada" @@ -627,10 +627,10 @@ "message": "Iniciar sessão com a chave de acesso" }, "invalidPasskeyPleaseTryAgain": { - "message": "Invalid Passkey. Please try again." + "message": "Senha inválida. Por favor, tente novamente." }, "twoFactorForPasskeysNotSupportedOnClientUpdateToLogIn": { - "message": "2FA for passkeys is not supported. Update the app to log in." + "message": "2FA para senhas não é suportada. Atualize o aplicativo para iniciar a sessão." }, "loginWithPasskeyInfo": { "message": "Use uma senha gerada que fará o login automaticamente sem uma senha. Biometrias como reconhecimento facial ou impressão digital, ou outro método de segurança FIDO2 verificarão sua identidade." @@ -672,7 +672,7 @@ "message": "Criptografia não suportada" }, "enablePasskeyEncryption": { - "message": "Set up encryption" + "message": "Configurar criptografia" }, "usedForEncryption": { "message": "Usado para criptografia" @@ -1359,11 +1359,11 @@ "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)" }, "onboardingImportDataDetailsPartTwoNoOrgs": { - "message": " instead.", + "message": " em vez disso.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead." }, "onboardingImportDataDetailsPartTwoWithOrgs": { - "message": " instead. You may need to wait until your administrator confirms your organization membership.", + "message": " em vez disso, você pode precisar esperar até que o seu administrador confirme a sua associação à organização.", "description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. You may need to wait until your administrator confirms your organization membership." }, "importError": { @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites Inseguros Encontrados" }, - "unsecuredWebsitesFoundDesc": { - "message": "Nós encontramos $COUNT$ item(ns) no seu cofre com URIs não protegido(s). Você deve alterar o esquema de URI para https:// se o site permitir.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Nós encontramos $COUNT$ item(ns) no seu cofre com URIs não segura(s). Você deve alterar o esquema de URI para https:// se o site permitir.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Credenciais Sem 2FA Encontradas" }, - "inactive2faFoundDesc": { - "message": "Encontramos $COUNT$ site(s) no seu cofre que pode(m) não estar configurados com login em duas etapas (segundo o site 2fa.directory). Para proteger ainda mais essas contas, você deve configurar o login em duas etapas.", + "inactive2faFoundReportDesc": { + "message": "Encontramos $COUNT$ site(s) no seu $VAULT$ que pode(m) não estar configurado com o login em duas etapas (de acordo com 2fa. irectory). Para proteger ainda mais essas contas, você deve configurar o login em duas etapas.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Senhas Expostas Encontradas" }, - "exposedPasswordsFoundDesc": { + "exposedPasswordsFoundReportDesc": { "message": "Encontramos no seu cofre $COUNT$ item(ns) com senha(s) que foi(ram) exposta(s) em violações de dado conhecida. Você deve alterá-las para usar uma nova senha.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Senhas Fracas Encontradas" }, - "weakPasswordsFoundDesc": { + "weakPasswordsFoundReportDesc": { "message": "Encontramos $COUNT$ item(ns) no seu cofre com senha(s) que não é/são fortes. Você deve atualizá-las para usar senhas mais fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Senhas Reutilizadas Encontradas" }, - "reusedPasswordsFoundDesc": { + "reusedPasswordsFoundReportDesc": { "message": "Nós encontramos $COUNT$ senha(s) que esta(ão) sendo reutilizadas no seu cofre. Você deve alterá-los para um valor único.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2802,10 +2822,10 @@ "message": "CLI" }, "bitWebVault": { - "message": "Bitwarden Web vault" + "message": "Cofre Web do Bitwarden" }, "bitSecretsManager": { - "message": "Bitwarden Secrets Manager" + "message": "Gerenciador de Segredos Bitwarden" }, "loggedIn": { "message": "Conectado(a)." @@ -3489,7 +3509,7 @@ "message": "Defina um limite de vagas para sua assinatura. Quando esse limite for atingido, você não poderá convidar novos usuários." }, "limitSmSubscriptionDesc": { - "message": "Set a seat limit for your Secrets Manager subscription. Once this limit is reached, you will not be able to invite new members." + "message": "Defina um limite de assento para sua assinatura do Gerenciador Secretos. Uma vez que este limite for atingido, você não poderá convidar novos membros." }, "maxSeatLimit": { "message": "Limite Máximo de Vaga (opcional)", @@ -3621,7 +3641,7 @@ "message": "Atualizar Chave de Criptografia" }, "updateEncryptionSchemeDesc": { - "message": "We've changed the encryption scheme to provide better security. Update your encryption key now by entering your master password below." + "message": "Alteramos o esquema de criptografia para fornecer melhor segurança. Atualize sua chave de criptografia agora digitando sua senha mestra abaixo." }, "updateEncryptionKeyWarning": { "message": "Depois de atualizar sua chave de criptografia, é necessário encerrar e iniciar a sessão em todos os aplicativos do Bitwarden que você está usando atualmente (como o aplicativo móvel ou as extensões do navegador). Não encerrar e iniciar sessão (que baixa sua nova chave de criptografia) pode resultar em corrupção de dados. Nós tentaremos desconectá-lo automaticamente, mas isso pode demorar um pouco." @@ -3678,7 +3698,7 @@ "message": "Escolha quando o tempo limite do seu cofre irá se esgotar e execute a ação selecionada." }, "vaultTimeoutLogoutDesc": { - "message": "Choose when your vault will be logged out." + "message": "Escolha quando seu cofre será desconectado." }, "oneMinute": { "message": "1 minuto" @@ -4083,7 +4103,7 @@ "message": "Identificador SSO" }, "ssoIdentifierHintPartOne": { - "message": "Provide this ID to your members to login with SSO. To bypass this step, set up ", + "message": "Forneça esse ID aos seus membros para logar com SSO. Para ignorar essa etapa, configure ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Provide this ID to your members to login with SSO. To bypass this step, set up Domain verification'" }, "unlinkSso": { @@ -4695,7 +4715,7 @@ "message": "Inscrito na recuperação de conta" }, "withdrawAccountRecovery": { - "message": "Withdraw from account recovery" + "message": "Retirar da recuperação de conta" }, "enrollPasswordResetSuccess": { "message": "Inscrição com sucesso!" @@ -4704,7 +4724,7 @@ "message": "Retirada com sucesso!" }, "eventEnrollAccountRecovery": { - "message": "User $ID$ enrolled in account recovery.", + "message": "O usuário $ID$ se inscreveu na recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4713,7 +4733,7 @@ } }, "eventWithdrawAccountRecovery": { - "message": "User $ID$ withdrew from account recovery.", + "message": "O usuário $ID$ retirou da recuperação de conta.", "placeholders": { "id": { "content": "$1", @@ -4887,7 +4907,7 @@ "message": "Erro" }, "accountRecoveryManageUsers": { - "message": "Manage users must also be granted with the manage account recovery permission" + "message": "Gerenciar usuários também devem ser concedidos com a permissão de gerenciar a recuperação de contas" }, "setupProvider": { "message": "Configuração do Provedor" @@ -4951,13 +4971,13 @@ "message": "Crie uma nova organização de cliente que será associada a você como o provedor. Você poderá acessar e gerenciar esta organização." }, "newClient": { - "message": "New client" + "message": "Novo cliente" }, "addExistingOrganization": { "message": "Adicionar Organização Existente" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Adicionar nova organização" }, "myProvider": { "message": "Meu Provedor" @@ -5117,7 +5137,7 @@ "message": "Ativar preenchimento automático" }, "activateAutofillPolicyDesc": { - "message": "Activate the auto-fill on page load setting on the browser extension for all existing and new members." + "message": "Ative o autopreenchimento na configuração de carregamento de página na extensão do navegador para todos os membros existentes e novos." }, "experimentalFeature": { "message": "Sites comprometidos ou não confiáveis podem tomar vantagem do autopreenchimento ao carregar a página." @@ -5432,7 +5452,7 @@ "message": "Conector de Chave" }, "memberDecryptionKeyConnectorDescStart": { - "message": "Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The", + "message": "Conecte o login com SSO ao seu servidor de chave de descriptografia auto-hospedado. Usando essa opção, os membros não precisarão usar suas senhas mestres para descriptografar os dados", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescLink": { @@ -5440,7 +5460,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "memberDecryptionKeyConnectorDescEnd": { - "message": "are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.", + "message": "são necessários para configurar a descriptografia do conector chave. Contacte o suporte do Bitwarden para configurar a assistência.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Connect login with SSO to your self-hosted decryption key server. Using this option, members won’t need to use their master passwords to decrypt vault data. The require SSO authentication and single organization policies are required to set up Key Connector decryption. Contact Bitwarden Support for set up assistance.'" }, "keyConnectorPolicyRestriction": { @@ -5637,7 +5657,7 @@ } }, "exportingOrganizationVaultDesc": { - "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "organization": { "content": "$1", @@ -5940,13 +5960,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "Launch Duo and follow the steps to finish logging in." + "message": "Inicie o Duo e siga os passos para finalizar o login." }, "duoRequiredByOrgForAccount": { - "message": "Duo two-step login is required for your account." + "message": "A autenticação em duas etapas do Duo é necessária para sua conta." }, "launchDuo": { - "message": "Launch Duo" + "message": "Abrir o Duo" }, "turnOn": { "message": "Ligar" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder acesso às coleções adicionando-as a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Você só pode atribuir coleções que você gerencia." + }, "accessAllCollectionsDesc": { "message": "Conceder acesso a todas as coleções atuais e futuras." }, @@ -6680,10 +6703,10 @@ "message": "Reenviar código" }, "memberColumnHeader": { - "message": "Member" + "message": "Membro" }, "groupSlashMemberColumnHeader": { - "message": "Group/Member" + "message": "Grupo/Membro" }, "selectGroupsAndMembers": { "message": "Selecione grupos e membros" @@ -6707,10 +6730,10 @@ "message": "Convidar membro" }, "needsConfirmation": { - "message": "Needs confirmation" + "message": "Precisa de confirmação" }, "memberRole": { - "message": "Member role" + "message": "Função de membro" }, "moreFromBitwarden": { "message": "Mais do Bitwarden" @@ -6719,7 +6742,7 @@ "message": "Trocar Produtos" }, "freeOrgInvLimitReachedManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Upgrade to a paid plan to invite more members.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Faça upgrade para um plano pago para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6728,7 +6751,7 @@ } }, "freeOrgInvLimitReachedNoManageBilling": { - "message": "Free organizations may have up to $SEATCOUNT$ members. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $SEATCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "seatcount": { "content": "$1", @@ -6737,7 +6760,7 @@ } }, "teamsStarterPlanInvLimitReachedManageBilling": { - "message": "Teams Starter plans may have up to $SEATCOUNT$ members. Upgrade to your plan to invite more members.", + "message": "Planos de Times Starter podem ter até $SEATCOUNT$ membros. Atualize para o seu plano para convidar mais membros.", "placeholders": { "seatcount": { "content": "$1", @@ -6755,7 +6778,7 @@ } }, "freeOrgMaxCollectionReachedManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Upgrade to a paid plan to add more collections.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ coleções. Faça o upgrade para um plano pago para adicionar mais coleções.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6764,7 +6787,7 @@ } }, "freeOrgMaxCollectionReachedNoManageBilling": { - "message": "Free organizations may have up to $COLLECTIONCOUNT$ collections. Contact your organization owner to upgrade.", + "message": "Organizações gratuitas podem ter até $COLLECTIONCOUNT$ membros. Entre em contato com o proprietário da sua organização para fazer upgrade.", "placeholders": { "COLLECTIONCOUNT": { "content": "$1", @@ -6779,10 +6802,10 @@ "message": "Exportar dados" }, "exportingOrganizationSecretDataTitle": { - "message": "Exporting Organization Secret Data" + "message": "Exportando dados secretos da organização" }, "exportingOrganizationSecretDataDescription": { - "message": "Only the Secrets Manager data associated with $ORGANIZATION$ will be exported. Items in other products or from other organizations will not be included.", + "message": "Apenas o cofre da organização associado com $ORGANIZATION$ será exportado. Itens do cofre pessoal e itens de outras organizações não serão incluídos.", "placeholders": { "ORGANIZATION": { "content": "$1", @@ -6812,7 +6835,7 @@ "message": "Upload manual" }, "manualUploadDesc": { - "message": "If you do not want to opt into billing sync, manually upload your license here." + "message": "Se você não deseja optar pela sincronização de cobrança, carregue sua licença manualmente aqui." }, "syncLicense": { "message": "Sincronizar licença" @@ -6827,13 +6850,13 @@ "message": "Última sincronização de licença" }, "billingSyncHelp": { - "message": "Billing Sync help" + "message": "Ajuda da Sincronização de faturamento" }, "licensePaidFeaturesHelp": { - "message": "License paid features help" + "message": "Ajuda dos recursos de licença paga" }, "selfHostGracePeriodHelp": { - "message": "After your subscription expires, you have 60 days to apply an updated license file to your organization. Grace period ends $GRACE_PERIOD_END_DATE$.", + "message": "Após a expiração da assinatura, você tem 60 dias para aplicar um arquivo de licença atualizado à sua organização. Fim do período de carência $GRACE_PERIOD_END_DATE$.", "placeholders": { "GRACE_PERIOD_END_DATE": { "content": "$1", @@ -6845,28 +6868,28 @@ "message": "Enviar licença" }, "projectPeopleDescription": { - "message": "Grant groups or people access to this project." + "message": "Conceder acesso a este projeto ou grupos de pessoas." }, "projectPeopleSelectHint": { - "message": "Type or select people or groups" + "message": "Digite ou selecione pessoas ou grupos" }, "projectServiceAccountsDescription": { - "message": "Grant service accounts access to this project." + "message": "Conceder acesso a contas de serviço a este projeto." }, "projectServiceAccountsSelectHint": { - "message": "Type or select service accounts" + "message": "Digite ou selecione contas de serviço" }, "projectEmptyPeopleAccessPolicies": { - "message": "Add people or groups to start collaborating" + "message": "Adicione pessoas ou grupos para começar a colaborar" }, "projectEmptyServiceAccountAccessPolicies": { "message": "Adicione contas de serviço para conceder acesso" }, "serviceAccountPeopleDescription": { - "message": "Grant groups or people access to this service account." + "message": "Conceder acesso a esta conta de serviço a grupos ou pessoas." }, "serviceAccountProjectsDescription": { - "message": "Assign projects to this service account. " + "message": "Atribuir projetos para esta conta de serviço. " }, "serviceAccountEmptyProjectAccesspolicies": { "message": "Adicionar projetos para conceder acesso" @@ -6878,13 +6901,13 @@ "message": "Grupo/Usuário" }, "lowKdfIterations": { - "message": "Low KDF Iterations" + "message": "Iterações KDF baixas" }, "updateLowKdfIterationsDesc": { - "message": "Update your encryption settings to meet new security recommendations and improve account protection." + "message": "Atualize suas configurações de criptografia para atender às novas recomendações de segurança e melhorar a proteção da conta." }, "changeKdfLoggedOutWarning": { - "message": "Proceeding will log you out of all active sessions. You will need to log back in and complete two-step login setup. We recommend exporting your vault before changing your encryption settings to prevent data loss." + "message": "O processo desconectará você de todas as sessões ativas. Você precisará iniciar a sessão novamente e concluir a configuração de login em duas etapas. Recomendamos exportar seu cofre antes de alterar suas configurações de criptografia para evitar perda de dados." }, "secretsManager": { "message": "Gerenciador de Segredos" @@ -6924,7 +6947,7 @@ "message": "Ocorreu um erro ao tentar ler o arquivo de importação" }, "accessedSecret": { - "message": "Accessed secret $SECRET_ID$.", + "message": "$SECRET_ID$ secreto acessado.", "placeholders": { "secret_id": { "content": "$1", @@ -6996,10 +7019,10 @@ "message": "Seleção é necessária." }, "saPeopleWarningTitle": { - "message": "Access tokens still available" + "message": "Tokens de acesso ainda disponíveis" }, "saPeopleWarningMessage": { - "message": "Removing people from a service account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a service account." + "message": "Remover pessoas de uma conta de serviço não remove os tokens de acesso que criaram. Para a melhor prática de segurança, é recomendável revogar os tokens de acesso criados por pessoas removidas de uma conta de serviço." }, "smAccessRemovalWarningProjectTitle": { "message": "Remover acesso para esse projeto" @@ -7008,10 +7031,10 @@ "message": "Esta ação removerá seu acesso ao projeto." }, "smAccessRemovalWarningSaTitle": { - "message": "Remove access to this service account" + "message": "Remover acesso a essa conta de serviço" }, "smAccessRemovalWarningSaMessage": { - "message": "This action will remove your access to the service account." + "message": "Essa ação removerá seu acesso à conta de serviço." }, "removeAccess": { "message": "Remover acesso" @@ -7023,16 +7046,16 @@ "message": "Senha mestre exposta" }, "exposedMasterPasswordDesc": { - "message": "Password found in a data breach. Use a unique password to protect your account. Are you sure you want to use an exposed password?" + "message": "A senha foi encontrada em violação de dados. Use uma senha única para proteger sua conta. Tem certeza de que deseja usar uma senha exposta?" }, "weakAndExposedMasterPassword": { - "message": "Weak and Exposed Master Password" + "message": "Senha Mestra Fraca e Exposta" }, "weakAndBreachedMasterPasswordDesc": { - "message": "Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password?" + "message": "Senha fraca identificada e encontrada em um vazamento de dados. Use uma senha forte e única para proteger a sua conta. Tem certeza de que deseja usar essa senha?" }, "characterMinimum": { - "message": "$LENGTH$ character minimum", + "message": "Mínimo de $LENGTH$ caracteres", "placeholders": { "length": { "content": "$1", @@ -7041,7 +7064,7 @@ } }, "masterPasswordMinimumlength": { - "message": "Master password must be at least $LENGTH$ characters long.", + "message": "A senha mestra deve ter pelo menos $LENGTH$ caracteres.", "placeholders": { "length": { "content": "$1", @@ -7057,14 +7080,14 @@ "message": "Descartar" }, "notAvailableForFreeOrganization": { - "message": "This feature is not available for free organizations. Contact your organization owner to upgrade." + "message": "Este recurso não está disponível para organizações gratuitas. Entre em contato com o seu proprietário para atualizar." }, "smProjectSecretsNoItemsNoAccess": { - "message": "Contact your organization's admin to manage secrets for this project.", + "message": "Entre em contato com o administrador da sua organização para gerenciar segredos para este projeto.", "description": "The message shown to the user under a project's secrets tab when the user only has read access to the project." }, "enforceOnLoginDesc": { - "message": "Require existing members to change their passwords" + "message": "Exigir que os membros existentes alterem suas senhas" }, "smProjectDeleteAccessRestricted": { "message": "Você não tem permissão para excluir este projeto", @@ -7081,16 +7104,16 @@ "message": "Sessão iniciada" }, "deviceApprovalRequired": { - "message": "Device approval required. Select an approval option below:" + "message": "Aprovação do dispositivo necessária. Selecione uma opção de aprovação abaixo:" }, "rememberThisDevice": { "message": "Lembrar deste dispositivo" }, "uncheckIfPublicDevice": { - "message": "Uncheck if using a public device" + "message": "Desmarque se estiver usando um dispositivo público" }, "approveFromYourOtherDevice": { - "message": "Approve from your other device" + "message": "Aprovar do seu outro dispositivo" }, "requestAdminApproval": { "message": "Solicitar aprovação do administrador" @@ -7099,17 +7122,17 @@ "message": "Aprovar com a senha mestre" }, "trustedDeviceEncryption": { - "message": "Trusted device encryption" + "message": "Criptografia de dispositivo confiável" }, "trustedDevices": { "message": "Dispositivos confiáveis" }, "memberDecryptionOptionTdeDescriptionPartOne": { - "message": "Once authenticated, members will decrypt vault data using a key stored on their device. The", + "message": "Uma vez autenticados, os membros descriptografarão os dados do cofre usando uma chave armazenada no seu dispositivo", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO Required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkOne": { - "message": "single organization", + "message": "organização única", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartTwo": { @@ -7117,7 +7140,7 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkTwo": { - "message": "SSO required", + "message": "Necessário SSO", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartThree": { @@ -7125,11 +7148,11 @@ "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionLinkThree": { - "message": "account recovery administration", + "message": "gerenciar recuperação de conta", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "memberDecryptionOptionTdeDescriptionPartFour": { - "message": "policy with automatic enrollment will turn on when this option is used.", + "message": "política com inscrição automática será ativada quando esta opção for utilizada.", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Once authenticated, members will decrypt vault data using a key stored on their device. The single organization policy, SSO required policy, and account recovery administration policy with automatic enrollment will turn on when this option is used.'" }, "orgPermissionsUpdatedMustSetPassword": { @@ -7157,7 +7180,7 @@ "message": "Recuperar conta" }, "updatedTempPassword": { - "message": "User updated a password issued through account recovery." + "message": "O usuário atualizou uma senha emitida através da recuperação de conta." }, "activatedAccessToSecretsManager": { "message": "Acesso ativado ao Gerenciador de Segredos", @@ -7232,7 +7255,7 @@ "message": "Remover membros que não têm senhas mestres sem definir uma para eles pode restringir o acesso à sua conta completa." }, "approvedAuthRequest": { - "message": "Approved device for $ID$.", + "message": "Dispositivo aprovado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7241,7 +7264,7 @@ } }, "rejectedAuthRequest": { - "message": "Denied device for $ID$.", + "message": "Dispositivo negado para $ID$.", "placeholders": { "id": { "content": "$1", @@ -7250,7 +7273,7 @@ } }, "requestedDeviceApproval": { - "message": "Requested device approval." + "message": "Aprovação do dispositivo solicitada." }, "startYour7DayFreeTrialOfBitwardenFor": { "message": "Comece o seu período de teste gratuito de 7 dias do Bitwarden para $ORG$", @@ -7262,7 +7285,7 @@ } }, "startYour7DayFreeTrialOfBitwardenSecretsManagerFor": { - "message": "Start your 7-Day free trial of Bitwarden Secrets Manager for $ORG$", + "message": "Inicie o seu período de teste gratuito de 7 dias do Bitwarden Secrets Manager para $ORG$", "placeholders": { "org": { "content": "$1", @@ -7280,19 +7303,19 @@ "message": "Sinalização de região selecionada" }, "accountSuccessfullyCreated": { - "message": "Account successfully created!" + "message": "Conta criada com sucesso!" }, "adminApprovalRequested": { - "message": "Admin approval requested" + "message": "Aprovação do administrador necessária" }, "adminApprovalRequestSentToAdmins": { - "message": "Your request has been sent to your admin." + "message": "Seu pedido foi enviado para seu administrador." }, "youWillBeNotifiedOnceApproved": { - "message": "You will be notified once approved." + "message": "Será notificado assim que for aprovado." }, "troubleLoggingIn": { - "message": "Trouble logging in?" + "message": "Problemas em efetuar login?" }, "loginApproved": { "message": "Sessão aprovada" @@ -7324,10 +7347,10 @@ } }, "secretsManagerForPlanDesc": { - "message": "For engineering and DevOps teams to manage secrets throughout the software development lifecycle." + "message": "Para equipes de engenharia e DevOps gerenciar segredos durante todo o ciclo de vida do desenvolvimento de software." }, "free2PersonOrganization": { - "message": "Free 2-person Organizations" + "message": "Organizações gratuitas com 2 pessoas" }, "unlimitedSecrets": { "message": "Segredos ilimitados" @@ -7366,7 +7389,7 @@ "message": "Assine o Gerenciador de Segredos" }, "addSecretsManagerUpgradeDesc": { - "message": "Add Secrets Manager to your upgraded plan to maintain access to any secrets created with your previous plan." + "message": "Adicione o Gerenciador de Segredos ao seu plano atualizado para manter o acesso a todos os segredos criados com seu plano anterior." }, "additionalServiceAccounts": { "message": "Contas de serviço adicionais" @@ -7420,13 +7443,13 @@ "message": "Limitar contas de serviço (opcional)" }, "limitServiceAccountsDesc": { - "message": "Set a limit for your service accounts. Once this limit is reached, you will not be able to create new service accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "serviceAccountLimit": { "message": "Limite de contas de serviço (opcional)" }, "maxServiceAccountCost": { - "message": "Max potential service account cost" + "message": "Custo máximo da conta de serviço potencial" }, "loggedInExclamation": { "message": "Conectado!" @@ -7450,7 +7473,7 @@ "message": "Já tem uma conta?" }, "skipToContent": { - "message": "Skip to content" + "message": "Pular para o conteúdo" }, "managePermissionRequired": { "message": "Pelo menos um membro ou grupo deve ter poder gerenciar a permissão." @@ -7459,10 +7482,10 @@ "message": "Chave de acesso" }, "passkeyNotCopied": { - "message": "Passkey will not be copied" + "message": "A senha não será copiada" }, "passkeyNotCopiedAlert": { - "message": "The passkey will not be copied to the cloned item. Do you want to continue cloning this item?" + "message": "A senha não será copiada para o item clonado. Deseja continuar clonando este item?" }, "modifiedCollectionManagement": { "message": "Definição de gerenciamento da coleção $ID$ modificada.", @@ -7484,7 +7507,7 @@ "message": "Use a extensão para salvar rapidamente credenciais e formulários de autopreenchimento sem abrir o aplicativo web." }, "projectAccessUpdated": { - "message": "Project access updated" + "message": "Acesso ao projeto atualizado" }, "unexpectedErrorSend": { "message": "Ocorreu um erro inesperado ao carregar este Envio. Tente novamente mais tarde." @@ -7502,13 +7525,13 @@ "message": "O acesso à coleção está restrito" }, "readOnlyCollectionAccess": { - "message": "You do not have access to manage this collection." + "message": "Você não tem acesso para gerenciar esta coleção." }, "grantCollectionAccess": { - "message": "Grant groups or members access to this collection." + "message": "Conceder acesso de grupos ou membros a esta coleção." }, "grantCollectionAccessMembersOnly": { - "message": "Grant members access to this collection." + "message": "Conceder acesso a essa coleção." }, "adminCollectionAccess": { "message": "Administrators can access and manage collections." @@ -7557,57 +7580,60 @@ "message": "Esta ação é irreversível" }, "confirmCollectionEnhancementsDialogContent": { - "message": "Turning on this feature will deprecate the manager role and replace it with a Can manage permission. This will take a few moments. Do not make any organization changes until it is complete. Are you sure you want to proceed?" + "message": "Ativar este recurso irá depreciar a função de gerente e substituí-lo por uma Permissão de para Gerenciar. Isso levará algum tempo. Não faça nenhuma alteração de organização até que esteja concluída. Tem certeza que deseja continuar?" }, "sorryToSeeYouGo": { - "message": "Sorry to see you go! Help improve Bitwarden by sharing why you're canceling.", + "message": "Lamentamos vê-lo ir! Ajude a melhorar o Bitwarden compartilhando o motivo de você estar cancelando.", "description": "A message shown to users as part of an offboarding survey asking them to provide more information on their subscription cancelation." }, "selectCancellationReason": { - "message": "Select a reason for canceling", + "message": "Selecione o motivo do cancelamento", "description": "Used as a form field label for a select input on the offboarding survey." }, "anyOtherFeedback": { - "message": "Is there any other feedback you'd like to share?", + "message": "Existe algum outro comentário que você gostaria de compartilhar?", "description": "Used as a form field label for a textarea input on the offboarding survey." }, "missingFeatures": { - "message": "Missing features", + "message": "Recursos ausentes", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "movingToAnotherTool": { - "message": "Moving to another tool", + "message": "Trocando de ferramenta", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooDifficultToUse": { - "message": "Too difficult to use", + "message": "Muito difícil de usar", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "notUsingEnough": { - "message": "Not using enough", + "message": "Não está usando o suficiente", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooExpensive": { - "message": "Too expensive", + "message": "Muito caro", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "freeForOneYear": { "message": "Grátis por 1 ano" }, "newWebApp": { - "message": "Welcome to the new and improved web app. Learn more about what’s changed." + "message": "Bem-vindo ao novo e melhorado aplicativo da web. Saiba mais sobre o que mudou." }, "releaseBlog": { - "message": "Read release blog" + "message": "Ler o blog do lançamento" }, "adminConsole": { - "message": "Admin Console" + "message": "Painel de administração" }, "providerPortal": { - "message": "Provider Portal" + "message": "Portal do provedor" + }, + "success": { + "message": "Success" }, "viewCollection": { - "message": "View collection" + "message": "Ver Coleção" }, "restrictedGroupAccess": { "message": "Você não pode se adicionar aos grupos." @@ -7616,28 +7642,28 @@ "message": "Você não pode se adicionar às coleções." }, "assign": { - "message": "Assign" + "message": "Atribuir" }, "assignToCollections": { - "message": "Assign to collections" + "message": "Atribuir à coleções" }, "assignToTheseCollections": { - "message": "Assign to these collections" + "message": "Atribuir a estas coleções" }, "bulkCollectionAssignmentDialogDescription": { - "message": "Select the collections that the items will be shared with. Once an item is updated in one collection, it will be reflected in all collections. Only organization members with access to these collections will be able to see the items." + "message": "Selecione as coleções com as quais os itens serão compartilhados. Assim que um item for atualizado em uma coleção, isso será refletido em todas as coleções. Apenas membros da organização com acesso a essas coleções poderão ver os itens." }, "selectCollectionsToAssign": { - "message": "Select collections to assign" + "message": "Selecione as coleções para atribuir" }, "noCollectionsAssigned": { - "message": "No collections have been assigned" + "message": "Nenhuma coleção foi atribuída" }, "successfullyAssignedCollections": { - "message": "Successfully assigned collections" + "message": "Coleções atribuídas com sucesso" }, "bulkCollectionAssignmentWarning": { - "message": "You have selected $TOTAL_COUNT$ items. You cannot update $READONLY_COUNT$ of the items because you do not have edit permissions.", + "message": "Você selecionou $TOTAL_COUNT$ itens. Você não pode atualizar $READONLY_COUNT$ destes itens porque você não tem permissão para edição.", "placeholders": { "total_count": { "content": "$1", @@ -7650,55 +7676,55 @@ } }, "items": { - "message": "Items" + "message": "Itens" }, "assignedSeats": { - "message": "Assigned seats" + "message": "Lugares Atribuídos" }, "assigned": { - "message": "Assigned" + "message": "Atribuído" }, "used": { - "message": "Used" + "message": "Utilizado" }, "remaining": { - "message": "Remaining" + "message": "Restante" }, "unlinkOrganization": { - "message": "Unlink organization" + "message": "Desvincular organização" }, "manageSeats": { - "message": "MANAGE SEATS" + "message": "GERENCIAR LUGARES" }, "manageSeatsDescription": { - "message": "Adjustments to seats will be reflected in the next billing cycle." + "message": "Os ajustes nos lugares serão refletidos no próximo ciclo de faturamento." }, "unassignedSeatsDescription": { - "message": "Unassigned subscription seats" + "message": "Assinatura de assentos desvinculada" }, "purchaseSeatDescription": { - "message": "Additional seats purchased" + "message": "Assentos adicionais comprados" }, "assignedSeatCannotUpdate": { - "message": "Assigned Seats can not be updated. Please contact your organization owner for assistance." + "message": "Os assentos atribuídos não podem ser atualizados. Por favor, entre em contato com o proprietário da sua organização para obter assistência." }, "subscriptionUpdateFailed": { - "message": "Subscription update failed" + "message": "Atualização da assinatura falhou" }, "trial": { - "message": "Trial", + "message": "Avaliação", "description": "A subscription status label." }, "pastDue": { - "message": "Past due", + "message": "Atrasado", "description": "A subscription status label" }, "subscriptionExpired": { - "message": "Subscription expired", + "message": "Assinatura expirada", "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "You have a grace period of $DAYS$ days from your subscription expiration date to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de carência de $DAYS$ dias a contar da data de expiração para manter sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7738,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "You have a grace period of $DAYS$ days from the date your first unpaid invoice is due to maintain your subscription. Please resolve the past due invoices by $SUSPENSION_DATE$.", + "message": "Você tem um período de licença de $DAYS$, contados a partir da data de vencimento da sua primeira fatura em aberto para manter a sua assinatura. Por favor, resolva as faturas vencidas até $SUSPENSION_DATE$.", "placeholders": { "days": { "content": "$1", @@ -7726,54 +7752,54 @@ "description": "A warning shown to the user when their subscription is past due and they pay via invoice." }, "unpaidInvoice": { - "message": "Unpaid invoice", + "message": "Fatura não paga", "description": "The header of a warning box shown to a user whose subscription is unpaid." }, "toReactivateYourSubscription": { - "message": "To reactivate your subscription, please resolve the past due invoices.", + "message": "Para reativar sua assinatura, por favor resolva as faturas vencidas.", "description": "The body of a warning box shown to a user whose subscription is unpaid." }, "cancellationDate": { - "message": "Cancellation date", + "message": "Data de cancelamento", "description": "The date header used when a subscription is cancelled." }, "machineAccountsCannotCreate": { - "message": "Machine accounts cannot be created in suspended organizations. Please contact your organization owner for assistance." + "message": "Contas de máquina não podem ser criadas em organizações suspensas. Entre em contato com o proprietário da organização para obter assistência." }, "machineAccount": { - "message": "Machine account", + "message": "Conta de máquina", "description": "A machine user which can be used to automate processes and access secrets in the system." }, "machineAccounts": { - "message": "Machine accounts", + "message": "Contas de máquina", "description": "The title for the section that deals with machine accounts." }, "newMachineAccount": { - "message": "New machine account", + "message": "Nova conta de máquina", "description": "Title for creating a new machine account." }, "machineAccountsNoItemsMessage": { - "message": "Create a new machine account to get started automating secret access.", + "message": "Crie uma nova conta de máquina para começar a automatizar o acesso secreto.", "description": "Message to encourage the user to start creating machine accounts." }, "machineAccountsNoItemsTitle": { - "message": "Nothing to show yet", + "message": "Ainda não há nada a ser exibido", "description": "Title to indicate that there are no machine accounts to display." }, "deleteMachineAccounts": { - "message": "Delete machine accounts", + "message": "Excluir contas de máquina", "description": "Title for the action to delete one or multiple machine accounts." }, "deleteMachineAccount": { - "message": "Delete machine account", + "message": "Excluir conta de máquina", "description": "Title for the action to delete a single machine account." }, "viewMachineAccount": { - "message": "View machine account", + "message": "Ver conta de máquina", "description": "Action to view the details of a machine account." }, "deleteMachineAccountDialogMessage": { - "message": "Deleting machine account $MACHINE_ACCOUNT$ is permanent and irreversible.", + "message": "A exclusão da conta de máquina $MACHINE_ACCOUNT$ é permanente e irreversível.", "placeholders": { "machine_account": { "content": "$1", @@ -7782,10 +7808,10 @@ } }, "deleteMachineAccountsDialogMessage": { - "message": "Deleting machine accounts is permanent and irreversible." + "message": "Excluir contas de máquina é permanente e irreversível." }, "deleteMachineAccountsConfirmMessage": { - "message": "Delete $COUNT$ machine accounts", + "message": "Excluir $COUNT$ contas de máquina", "placeholders": { "count": { "content": "$1", @@ -7794,60 +7820,60 @@ } }, "deleteMachineAccountToast": { - "message": "Machine account deleted" + "message": "Conta de máquina excluída" }, "deleteMachineAccountsToast": { - "message": "Machine accounts deleted" + "message": "Contas de máquina excluídas" }, "searchMachineAccounts": { - "message": "Search machine accounts", + "message": "Pesquisar contas de máquina", "description": "Placeholder text for searching machine accounts." }, "editMachineAccount": { - "message": "Edit machine account", + "message": "Editar conta da máquina", "description": "Title for editing a machine account." }, "machineAccountName": { - "message": "Machine account name", + "message": "Nome da conta de máquina", "description": "Label for the name of a machine account" }, "machineAccountCreated": { - "message": "Machine account created", + "message": "Conta de máquina criada", "description": "Notifies that a new machine account has been created" }, "machineAccountUpdated": { - "message": "Machine account updated", + "message": "Conta de máquina atualizada", "description": "Notifies that a machine account has been updated" }, "projectMachineAccountsDescription": { - "message": "Grant machine accounts access to this project." + "message": "Conceder acesso a contas de máquina a este projeto." }, "projectMachineAccountsSelectHint": { - "message": "Type or select machine accounts" + "message": "Digite ou selecione contas de máquina" }, "projectEmptyMachineAccountAccessPolicies": { - "message": "Add machine accounts to grant access" + "message": "Adicione contas de máquina para conceder acesso" }, "machineAccountPeopleDescription": { - "message": "Grant groups or people access to this machine account." + "message": "Conceder acesso de grupos ou pessoas a esta conta de máquina." }, "machineAccountProjectsDescription": { - "message": "Assign projects to this machine account. " + "message": "Atribuir projetos a esta conta de máquina. " }, "createMachineAccount": { - "message": "Create a machine account" + "message": "Criar uma conta de máquina" }, "maPeopleWarningMessage": { - "message": "Removing people from a machine account does not remove the access tokens they created. For security best practice, it is recommended to revoke access tokens created by people removed from a machine account." + "message": "Remover pessoas de uma conta de máquina não remove os tokens de acesso que elas criaram. Por melhores práticas de segurança, é recomendado revogar os tokens de acesso criados por pessoas que foram removidas de uma conta de máquina." }, "smAccessRemovalWarningMaTitle": { - "message": "Remove access to this machine account" + "message": "Remover acesso a esta conta de máquina" }, "smAccessRemovalWarningMaMessage": { - "message": "This action will remove your access to the machine account." + "message": "Esta ação irá remover seu acesso à conta de máquina." }, "machineAccountsIncluded": { - "message": "$COUNT$ machine accounts included", + "message": "$COUNT$ contas de serviço incluídas", "placeholders": { "count": { "content": "$1", @@ -7856,7 +7882,7 @@ } }, "additionalMachineAccountCost": { - "message": "$COST$ per month for additional machine accounts", + "message": "$COST$ mensal para contas de máquina adicionais", "placeholders": { "cost": { "content": "$1", @@ -7865,10 +7891,10 @@ } }, "additionalMachineAccounts": { - "message": "Additional machine accounts" + "message": "Contas de máquina adicionais" }, "includedMachineAccounts": { - "message": "Your plan comes with $COUNT$ machine accounts.", + "message": "Seu plano vem com $COUNT$ contas de máquina.", "placeholders": { "count": { "content": "$1", @@ -7877,7 +7903,7 @@ } }, "addAdditionalMachineAccounts": { - "message": "You can add additional machine accounts for $COST$ per month.", + "message": "Você pode adicionar contas de máquina extras por $COST$ mensais.", "placeholders": { "cost": { "content": "$1", @@ -7886,24 +7912,168 @@ } }, "limitMachineAccounts": { - "message": "Limit machine accounts (optional)" + "message": "Limitar contas de máquina (opcional)" }, "limitMachineAccountsDesc": { - "message": "Set a limit for your machine accounts. Once this limit is reached, you will not be able to create new machine accounts." + "message": "Defina um limite para suas contas de máquina. Quando este limite for atingido, você não poderá criar novas contas de máquina." }, "machineAccountLimit": { - "message": "Machine account limit (optional)" + "message": "Limite de conta de máquina (opcional)" }, "maxMachineAccountCost": { - "message": "Max potential machine account cost" + "message": "Custo máximo de conta de máquina" }, "machineAccountAccessUpdated": { - "message": "Machine account access updated" + "message": "Acesso a conta de máquina atualizado" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Você não pode adicionar você mesmo a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Itens de organização não atribuídos não estão mais visíveis na sua tela Todos os Cofres através dos dispositivos e agora só são acessíveis por meio do Console de Administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: Em 16 de maio, 2024, itens da organização que não foram atribuídos não estarão mais visíveis em sua visualização de Todos os Cofres dos dispositivos e só serão acessíveis por meio do painel de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para torná-los visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Excluir Provedor" + }, + "deleteProviderConfirmation": { + "message": "A exclusão de um provedor é permanente e irreversível. Digite sua senha mestra para confirmar a exclusão do provedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível excluir $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provedor excluído" + }, + "providerDeletedDesc": { + "message": "O provedor e todos os dados associados foram excluídos." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Você pediu para excluir este Provedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A exclusão do seu provedor é permanente. Não pode ser desfeita." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de cliente para gerenciar como um Provedor. Posições adicionais serão refletidas no próximo ciclo de faturamento." + }, + "selectAPlan": { + "message": "Selecione um plano" + }, + "thirtyFivePercentDiscount": { + "message": "35% de Desconto" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar Organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Alternar Menu da Organização" + }, + "vaultItemSelect": { + "message": "Selecionar item do cofre" + }, + "collectionItemSelect": { + "message": "Selecionar item da coleção" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gerenciar faturamento a partir do Portal do Provedor" } } diff --git a/apps/web/src/locales/pt_PT/messages.json b/apps/web/src/locales/pt_PT/messages.json index cf1ad10d9b..5446a9dd65 100644 --- a/apps/web/src/locales/pt_PT/messages.json +++ b/apps/web/src/locales/pt_PT/messages.json @@ -67,7 +67,7 @@ "message": "Número do passaporte" }, "licenseNumber": { - "message": "Número da licença" + "message": "Número da carta de condução" }, "email": { "message": "E-mail" @@ -365,7 +365,7 @@ "message": "Cidade / Localidade" }, "stateProvince": { - "message": "Estado / Província" + "message": "Estado / Região" }, "zipPostalCode": { "message": "Código postal" @@ -1135,10 +1135,10 @@ "message": "Pontuação mínima de complexidade" }, "minNumbers": { - "message": "Números mínimos" + "message": "Mínimo de números" }, "minSpecial": { - "message": "Caracteres especiais minímos", + "message": "Mínimo de caracteres especiais", "description": "Minimum special characters" }, "ambiguous": { @@ -1288,7 +1288,7 @@ "message": "Definições da chave de encriptação alteradas" }, "dangerZone": { - "message": "Zona de perigo" + "message": "Zona de risco" }, "dangerZoneDesc": { "message": "Cuidado, estas ações são irreversíveis!" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Sites inseguros encontrados" }, - "unsecuredWebsitesFoundDesc": { + "unsecuredWebsitesFoundReportDesc": { "message": "Encontrámos $COUNT$ itens no seu cofre com URIs não seguros. Deve alterar o esquema de URI para https:// se o site o permitir.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Credenciais sem verificação de dois passos encontrados" }, - "inactive2faFoundDesc": { - "message": "Encontrámos $COUNT$ site(s) no seu cofre que podem não ter a verificação de dois passos configurada (de acordo com 2fa.directory). Para proteger ainda mais essas contas, deve configurar a verificação de dois passos.", + "inactive2faFoundReportDesc": { + "message": "Encontrámos $COUNT$ site(s) no seu cofre que podem não estar configurados com a verificação de dois passos (de acordo com a 2fa.directory). Para proteger ainda mais estas contas, deve configurar a verificação de dois passos.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Palavras-passe expostas encontradas" }, - "exposedPasswordsFoundDesc": { - "message": "Encontrámos $COUNT$ itens no seu cofre que têm palavras-passe que foram expostas em violações de dados conhecidas. Deve alterá-los para utilizar uma nova palavra-passe.", + "exposedPasswordsFoundReportDesc": { + "message": "Encontrámos $COUNT$ itens no seu cofre que têm palavras-passe que foram expostas em violações de dados conhecidas. Deve alterá-los para utilizarem uma nova palavra-passe.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Palavras-passe fracas encontradas" }, - "weakPasswordsFoundDesc": { + "weakPasswordsFoundReportDesc": { "message": "Encontrámos $COUNT$ itens no seu cofre com palavras-passe que não são fortes. Deve atualizá-los para utilizarem palavras-passe mais fortes.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Palavras-passe reutilizadas encontradas" }, - "reusedPasswordsFoundDesc": { + "reusedPasswordsFoundReportDesc": { "message": "Encontrámos $COUNT$ palavras-passe que estão a ser reutilizadas no seu cofre. Deve alterá-las para um valor único.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -3706,7 +3726,7 @@ "description": "ex. Date this item was updated" }, "dateCreated": { - "message": "Criado a", + "message": "Criado", "description": "ex. Date this item was created" }, "datePasswordUpdated": { @@ -5955,7 +5975,7 @@ "message": "Ligado" }, "off": { - "message": "Desligado" + "message": "Desativado" }, "members": { "message": "Membros" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Conceder aos membros acesso às coleções adicionando-os a este grupo." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Só pode atribuir colecções que gere." + }, "accessAllCollectionsDesc": { "message": "Conceder acesso a todas as coleções atuais e futuras." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portal do fornecedor" }, + "success": { + "message": "Com sucesso" + }, "viewCollection": { "message": "Ver coleção" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Acesso à conta automática atualizado" }, - "unassignedItemsBanner": { - "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na sua vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + "restrictedGroupAccessDesc": { + "message": "Não se pode adicionar a si próprio a um grupo." }, "unassignedItemsBannerSelfHost": { "message": "Aviso: A 2 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração. Atribua estes itens a uma coleção a partir da Consola de administração para os tornar visíveis." + }, + "unassignedItemsBannerNotice": { + "message": "Aviso: Os itens da organização não atribuídos já não são visíveis na vista Todos os cofres em todos os dispositivos e agora só estão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Aviso: A 16 de maio de 2024, os itens da organização não atribuídos deixarão de estar visíveis na vista Todos os cofres em todos os dispositivos e só estarão acessíveis através da Consola de administração." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Atribua estes itens a uma coleção a partir da", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "para os tornar visíveis.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Eliminar fornecedor" + }, + "deleteProviderConfirmation": { + "message": "A eliminação de um fornecedor é permanente e irreversível. Introduza a sua palavra-passe mestra para confirmar a eliminação do fornecedor e de todos os dados associados." + }, + "deleteProviderName": { + "message": "Não é possível eliminar $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "É necessário desvincular todos os clientes antes de poder eliminar $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Fornecedor eliminado" + }, + "providerDeletedDesc": { + "message": "O fornecedor e todos os dados associados foram eliminados." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Pediu para eliminar este fornecedor. Clique no botão abaixo para confirmar." + }, + "deleteProviderWarning": { + "message": "A eliminação do seu fornecedor é permanente. Não pode ser anulada." + }, + "errorAssigningTargetCollection": { + "message": "Erro ao atribuir a coleção de destino." + }, + "errorAssigningTargetFolder": { + "message": "Erro ao atribuir a pasta de destino." + }, + "integrationsAndSdks": { + "message": "Integrações e SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrações" + }, + "integrationsDesc": { + "message": "Sincronize automaticamente segredos do Gestor de Segredos do Bitwarden para um serviço de terceiros." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Utilize o SDK do Gestor de Segredos do Bitwarden nas seguintes linguagens de programação para criar as suas próprias aplicações." + }, + "setUpGithubActions": { + "message": "Configurar ações do Github" + }, + "setUpGitlabCICD": { + "message": "Configurar o GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Configurar o Ansible" + }, + "cSharpSDKRepo": { + "message": "Ver repositório de C#" + }, + "cPlusPlusSDKRepo": { + "message": "Ver repositório de C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Ver repositório de JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Ver repositório de Java" + }, + "pythonSDKRepo": { + "message": "Ver repositório de Python" + }, + "phpSDKRepo": { + "message": "Ver repositório de php" + }, + "rubySDKRepo": { + "message": "Ver repositório de Ruby" + }, + "goSDKRepo": { + "message": "Ver repositório de Go" + }, + "createNewClientToManageAsProvider": { + "message": "Crie uma nova organização de clientes para gerir como Fornecedor. Os lugares adicionais serão refletidos na próxima faturação." + }, + "selectAPlan": { + "message": "Selecionar um plano" + }, + "thirtyFivePercentDiscount": { + "message": "Desconto de 35%" + }, + "monthPerMember": { + "message": "mês por membro" + }, + "seats": { + "message": "Lugares" + }, + "addOrganization": { + "message": "Adicionar organização" + }, + "createdNewClient": { + "message": "Novo cliente criado com sucesso" + }, + "noAccess": { + "message": "Sem acesso" + }, + "collectionAdminConsoleManaged": { + "message": "Esta coleção só é acessível a partir da Consola de administração" + }, + "organizationOptionsMenu": { + "message": "Alternar menu da organização" + }, + "vaultItemSelect": { + "message": "Selecionar item do cofre" + }, + "collectionItemSelect": { + "message": "Selecionar item da coleção" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Gira a faturação a partir do Portal do fornecedor" } } diff --git a/apps/web/src/locales/ro/messages.json b/apps/web/src/locales/ro/messages.json index ba5badd908..eb40a84da9 100644 --- a/apps/web/src/locales/ro/messages.json +++ b/apps/web/src/locales/ro/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "S-au găsit website-uri nesecurizate" }, - "unsecuredWebsitesFoundDesc": { - "message": "Am găsit $COUNT$ articole în seiful dvs. cu URI-uri nesecurizate. Ar trebui să schimbați schema URl-urilor lor în https:// dacă saitul o permite.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "S-au găsit autentificări fără autentificare în două etape" }, - "inactive2faFoundDesc": { - "message": "Am găsit $COUNT$ website(-uri) în seiful dvs. care s-ar putea să nu fie configurate cu autentificare în două etape (în conformitate cu 2fa.directory). Pentru a proteja în continuare aceste conturi, ar trebui să configurați autentificarea în două etape.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Parolele expuse găsite" }, - "exposedPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ articole în seiful dvs. care folosesc parole dezvăluite în scurgeri de date cunoscute. Ar trebui să le schimbați pentru a utiliza o parolă nouă.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Parole slabe găsite" }, - "weakPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ articole cu parole slabe articole în seiful dvs. Ar trebui să le actualizați ca să folosească parole puternice.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "S-au găsit parole refolosite" }, - "reusedPasswordsFoundDesc": { - "message": "Am găsit $COUNT$ parole reutilizate în seiful dvs. Ar trebui să le schimbați la o valoare unică.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/ru/messages.json b/apps/web/src/locales/ru/messages.json index eeb89acf55..92eced8799 100644 --- a/apps/web/src/locales/ru/messages.json +++ b/apps/web/src/locales/ru/messages.json @@ -1,6 +1,6 @@ { "whatTypeOfItem": { - "message": "Какой это тип элемента?" + "message": "Выберите тип элемента" }, "name": { "message": "Название" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Найдены незащищенные сайты" }, - "unsecuredWebsitesFoundDesc": { - "message": "В хранилище обнаружены элементы ($COUNT$ шт.) с незащищенными URI. Вам следует изменить их схему URI на https://, если сайт это позволяет.", + "unsecuredWebsitesFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.) с незащищенными URI. Вам следует изменить их схему URI на https://, если сайт это позволяет.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Найдены логины без двухэтапной аутентификации" }, - "inactive2faFoundDesc": { - "message": "В хранилище обнаружены сайты ($COUNT$ шт.), у которых может быть не настроена двухэтапная аутентификация (согласно 2fa.directory). Для дополнительной защиты этих аккаунтов следует настроить двухэтапную аутентификацию.", + "inactive2faFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены сайты ($COUNT$ шт.), у которых может быть не настроена двухэтапная аутентификация (согласно 2fa.directory). Для дополнительной защиты этих аккаунтов следует настроить двухэтапную аутентификацию.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Найдены скомпрометированные пароли" }, - "exposedPasswordsFoundDesc": { - "message": "В хранилище обнаружены элементы ($COUNT$ шт.), пароли которых скомпрометированы. Вам следует задать для них новые пароли.", + "exposedPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.), пароли которых скомпрометированы. Вам следует задать для них новые пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Обнаружены слабые пароли" }, - "weakPasswordsFoundDesc": { - "message": "В хранилище есть элементы ($COUNT$ шт.) с ненадежными паролями. Следует задать для них более сильные пароли.", + "weakPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружены элементы ($COUNT$ шт.) с ненадежными паролями. Следует задать для них более сильные пароли.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Обнаружены повторно использованные пароли" }, - "reusedPasswordsFoundDesc": { - "message": "В хранилище есть элементы ($COUNT$ шт.) с повторно использованными паролями. Следует изменить их на уникальные.", + "reusedPasswordsFoundReportDesc": { + "message": "В вашем $VAULT$ обнаружена элементы ($COUNT$ шт.) с повторно использованными паролями. Следует изменить их на уникальные.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Предоставить доступ к коллекциям, при добавлении их в эту группу." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Вы можете назначать только те коллекции, которыми управляете." + }, "accessAllCollectionsDesc": { "message": "Предоставить доступ ко всем текущим и будущим коллекциям." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успешно" + }, "viewCollection": { "message": "Посмотреть коллекцию" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Доступ аккаунта компьютера обновлен" }, - "unassignedItemsBanner": { - "message": "Обратите внимание: неприсвоенные элементы организации больше не видны в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + "restrictedGroupAccessDesc": { + "message": "Нельзя добавить самого себя в группу." }, "unassignedItemsBannerSelfHost": { "message": "Уведомление: 2 мая 2024 года неприсвоенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора. Назначьте эти элементы коллекции в консоли администратора, чтобы сделать их видимыми." + }, + "unassignedItemsBannerNotice": { + "message": "Уведомление: Неприсвоенные элементы организации больше не отображаются в представлении \"Все хранилища\" на всех устройствах и теперь доступны только через консоль администратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Уведомление: с 16 мая 2024 года неназначенные элементы организации больше не будут отображаться в представлении \"Все хранилища\" на всех устройствах и будут доступны только через консоль администратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Назначьте эти элементы в коллекцию из", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "чтобы сделать их видимыми.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Удалить провайдера" + }, + "deleteProviderConfirmation": { + "message": "Удаление провайдера является постоянным и необратимым. Введите свой мастер-пароль, чтобы подтвердить удаление провайдера и всех связанных с ним данных." + }, + "deleteProviderName": { + "message": "Невозможно удалить $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Перед удалением $ID$ необходимо отвязать всех клиентов.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдер удален" + }, + "providerDeletedDesc": { + "message": "Провайдер и все связанные с ним данные были удалены." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Вы запросили удаление этого провайдера. Воспользуйтесь кнопкой ниже для подтверждения." + }, + "deleteProviderWarning": { + "message": "Удаление вашего провайдера необратимо. Это нельзя отменить." + }, + "errorAssigningTargetCollection": { + "message": "Ошибка при назначении целевой коллекции." + }, + "errorAssigningTargetFolder": { + "message": "Ошибка при назначении целевой папки." + }, + "integrationsAndSdks": { + "message": "Интеграции и SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Интеграции" + }, + "integrationsDesc": { + "message": "Автоматическая синхронизация секретов из Bitwarden Secrets Manager со сторонним сервисом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Используйте SDK Bitwarden Secrets Manager на следующих языках программирования для создания собственных приложений." + }, + "setUpGithubActions": { + "message": "Настроить Github Actions" + }, + "setUpGitlabCICD": { + "message": "Настроить GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Настроить Ansible" + }, + "cSharpSDKRepo": { + "message": "Просмотр репозитория C#" + }, + "cPlusPlusSDKRepo": { + "message": "Просмотр репозитория C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Просмотр репозитория JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Просмотр репозитория Java" + }, + "pythonSDKRepo": { + "message": "Просмотр репозитория Python" + }, + "phpSDKRepo": { + "message": "Просмотр репозитория php" + }, + "rubySDKRepo": { + "message": "Просмотр репозитория Ruby" + }, + "goSDKRepo": { + "message": "Просмотр репозитория Go" + }, + "createNewClientToManageAsProvider": { + "message": "Создайте новую клиентскую организацию для управления ею в качестве провайдера. Дополнительные места будут отражены в следующем биллинговом цикле." + }, + "selectAPlan": { + "message": "Выберите план" + }, + "thirtyFivePercentDiscount": { + "message": "Скидка 35%" + }, + "monthPerMember": { + "message": "в месяц за пользователя" + }, + "seats": { + "message": "Места" + }, + "addOrganization": { + "message": "Добавить организацию" + }, + "createdNewClient": { + "message": "Новый клиент успешно создан" + }, + "noAccess": { + "message": "Нет доступа" + }, + "collectionAdminConsoleManaged": { + "message": "Эта коллекция доступна только из консоли администратора" + }, + "organizationOptionsMenu": { + "message": "Переключить меню организации" + }, + "vaultItemSelect": { + "message": "Выбрать элемент хранилища" + }, + "collectionItemSelect": { + "message": "Выбрать элемент коллекции" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Управление биллингом на портале провайдера" } } diff --git a/apps/web/src/locales/si/messages.json b/apps/web/src/locales/si/messages.json index 0faa92c08d..f264457250 100644 --- a/apps/web/src/locales/si/messages.json +++ b/apps/web/src/locales/si/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sk/messages.json b/apps/web/src/locales/sk/messages.json index 67f5abdc7d..7dd89c23be 100644 --- a/apps/web/src/locales/sk/messages.json +++ b/apps/web/src/locales/sk/messages.json @@ -344,7 +344,7 @@ "message": "Krstné meno" }, "middleName": { - "message": "Druhé meno" + "message": "Stredné meno" }, "lastName": { "message": "Priezvisko" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Našli sa nezabezpečené stránky" }, - "unsecuredWebsitesFoundDesc": { - "message": "Našli sme $COUNT$ položky vo vašom trezore s nezabezpečenými URI. Ak to stránka podporuje, môžete zmeniť schému URI na https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Našli sa prihlásenia bez dvojstupňového overenia" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Našli sme uniknuté heslá" }, - "exposedPasswordsFoundDesc": { - "message": "Našli sme $COUNT$ položiek vo vašom trezore ktoré používajú uniknuté heslá. Mali by ste ich zmeniť aby používali nové heslá.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Našli sa slabé heslá" }, - "weakPasswordsFoundDesc": { - "message": "Našli sme $COUNT$ položiek vo vašom trezore, ktoré nepoužívajú silné heslá. Mali by ste ich aktualizovať a použiť silnejšie heslá.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Našli sa viacnásobne použité heslá" }, - "reusedPasswordsFoundDesc": { - "message": "Vo vašom trezore sme našli $COUNT$ hesiel, ktoré sú použité na viacerých stránkach. Mali by ste ich zmeniť aby každá stránka mala unikátne heslo.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2814,7 +2834,7 @@ "message": "Zmenené heslo k účtu." }, "enabledUpdated2fa": { - "message": "Dvojstupňové prihlasovanie zapnuté/aktualizované." + "message": "Dvojstupňové prihlasovanie uložené" }, "disabled2fa": { "message": "Dvojstupňové prihlasovanie vypnuté." @@ -3234,10 +3254,10 @@ "message": "Skupinový prístup" }, "groupAccessUserDesc": { - "message": "Upraviť skupiny, do ktorých patrí používateľ." + "message": "Povoľte členom prístup k zbierkam ich pridaním do jednej alebo viac skupín." }, "invitedUsers": { - "message": "Používatelia pozvaní." + "message": "Používatelia pozvaní" }, "resendInvitation": { "message": "Znovu poslať pozvánku" @@ -3246,7 +3266,7 @@ "message": "Znovu poslať e-mail" }, "hasBeenReinvited": { - "message": "$USER$ bol znovu pozvaný.", + "message": "$USER$ bol znovu pozvaný", "placeholders": { "user": { "content": "$1", @@ -3270,7 +3290,7 @@ } }, "confirmUsers": { - "message": "Potvrdiť používateľov" + "message": "Potvrdiť členov" }, "usersNeedConfirmed": { "message": "Máte používateľov, ktorí prijali pozvanie, ale ešte ich musíte potvrdiť. Používatelia nebudú mať prístup k organizácii, kým nebudú potvrdení." @@ -3294,13 +3314,13 @@ "message": "Skontrolujte si doručenú poštu, mali by ste obdržať odkaz pre verifikáciu." }, "emailVerified": { - "message": "Vaša emailová adresa bola overená." + "message": "Emailová adresa konta bola overená" }, "emailVerifiedFailed": { "message": "Overovanie zlyhalo. Skúste si odoslať nový verifikačný e-mail." }, "emailVerificationRequired": { - "message": "Vyžaduje sa overenie e-mailu" + "message": "Vyžaduje sa overenie e-mailom" }, "emailVerificationRequiredDesc": { "message": "Na používanie tejto funkcie musíte overiť svoj e-mail." @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Povoľte prístup k zbierkam ich pridaním do tejto skupiny." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Povoliť prístup k všetkým súčasným a budúcim zbierkam." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Portál poskytovateľa" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Pozrieť zbierku" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { - "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "message": "Upozornenie: 2. mája nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky Trezory a budú prístupné len cez administrátorskú konzolu. Aby boli viditeľné, priraďte tieto položky do kolekcie z konzoly administrátora." + }, + "unassignedItemsBannerNotice": { + "message": "Upozornenie: Nepriradené položky organizácie už nie sú viditeľné v zobrazení Všetky trezory a sú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Upozornenie: 16. mája 2024 nepriradené položky organizácie už nebudú viditeľné v zobrazení Všetky trezory a budú prístupné iba cez Správcovskú konzolu." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Priradiť tieto položky do zbierky zo", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": ", aby boli viditeľné.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Odstrániť poskytovateľa" + }, + "deleteProviderConfirmation": { + "message": "Odstránenie poskytovateľa je trvalé a nenávratné. Zadajte vaše hlavné heslo pre potvrdenie odstránenia poskytovateľa a súvisiacich dát." + }, + "deleteProviderName": { + "message": "$ID$ sa nedá odstrániť", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Poskytovateľ odstránený" + }, + "providerDeletedDesc": { + "message": "Poskytovateľ a všetky súvisiace dáta boli odstránené." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Požiadali ste o odstránenie tohto Poskytovateľa. Pre potvrdenie operácie použite tlačidlo nižšie." + }, + "deleteProviderWarning": { + "message": "Odstránenie poskytovateľa je trvalé. Nedá sa vrátiť späť." + }, + "errorAssigningTargetCollection": { + "message": "Chyba pri priraďovaní cieľovej kolekcie." + }, + "errorAssigningTargetFolder": { + "message": "Chyba pri priraďovaní cieľového priečinka." + }, + "integrationsAndSdks": { + "message": "Integrácie a SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrácie" + }, + "integrationsDesc": { + "message": "Automaticky synchronizovať položky z Bitwarden Secrets Manager do služby tretej strany." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Použite Bitwarden Secrets Manager SDK v následujúcich programovacích jazykoch pre vytvorenie vašej vlastnej aplikácie." + }, + "setUpGithubActions": { + "message": "Nastaviť Github Actions" + }, + "setUpGitlabCICD": { + "message": "Nastaviť GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Nastaviť Ansible" + }, + "cSharpSDKRepo": { + "message": "Zobraziť C# repozitár" + }, + "cPlusPlusSDKRepo": { + "message": "Zobraziť C++ repozitár" + }, + "jsWebAssemblySDKRepo": { + "message": "Zobraziť JS WebAssembly repozitár" + }, + "javaSDKRepo": { + "message": "Zobraziť Java repozitár" + }, + "pythonSDKRepo": { + "message": "Zobraziť Python repozitár" + }, + "phpSDKRepo": { + "message": "Zobraziť php repozitár" + }, + "rubySDKRepo": { + "message": "Zobraziť Ruby repozitár" + }, + "goSDKRepo": { + "message": "Zobraziť Go repozitár" + }, + "createNewClientToManageAsProvider": { + "message": "Vytvoriť novú klientskú organizáciu ktorú môžete spravovať ako Poskytovateľ. Dodatočné sedenia sa prejavia v najbližšom fakturačnom období." + }, + "selectAPlan": { + "message": "Vyberte plán" + }, + "thirtyFivePercentDiscount": { + "message": "Zľava 35%" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Sedenia" + }, + "addOrganization": { + "message": "Pridať organizáciu" + }, + "createdNewClient": { + "message": "Nový klient úspešne vytvorený" + }, + "noAccess": { + "message": "Žiadny prístup" + }, + "collectionAdminConsoleManaged": { + "message": "Táto zbierka je dostupná iba z administrátorskej konzoly" + }, + "organizationOptionsMenu": { + "message": "Prepnúť menu organizácie" + }, + "vaultItemSelect": { + "message": "Vyberte položku z trezora" + }, + "collectionItemSelect": { + "message": "Vyberte položku zo zbierky" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Spravujte fakturáciu cez portál poskytovateľa" } } diff --git a/apps/web/src/locales/sl/messages.json b/apps/web/src/locales/sl/messages.json index 6a55868df2..5a85e934f0 100644 --- a/apps/web/src/locales/sl/messages.json +++ b/apps/web/src/locales/sl/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Našli smo izpostavljena gesla" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Našli smo šibka gesla" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Našli smo podvojena gesla" }, - "reusedPasswordsFoundDesc": { - "message": "Našli smo toliko gesel, ki se uporabljajo na več mestih: $COUNT$. Morali bi jih spremeniti tako, da bo vsako drugačno.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sr/messages.json b/apps/web/src/locales/sr/messages.json index 9683c587b7..38bc66f911 100644 --- a/apps/web/src/locales/sr/messages.json +++ b/apps/web/src/locales/sr/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Пронађене су незаштићене веб странице" }, - "unsecuredWebsitesFoundDesc": { - "message": "Нашли смо $COUNT$ ставке у вашем сефу са незаштићеним УРЛ. Требали би да промените шеме у https:// ако веб страница то дозвољава.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Нађене пријаве без 2FA" }, - "inactive2faFoundDesc": { - "message": "Нашли смо $COUNT$ сајта у вашем сефу који можда нису конфигурисани за пријаву у два корака (према 2fa.directory). Да бисте додатно заштитили ове налоге, требало би да подесите пријаву у два корака.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Пронађене изложене лозинке" }, - "exposedPasswordsFoundDesc": { - "message": "Пронашли смо у вашем сефу $COUNT$ предмета који садрже лозинке откривене у познатим повредама података. Требали би да их промените да бисте користили нову лозинку.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Пронађене су слабе лозинке" }, - "weakPasswordsFoundDesc": { - "message": "Пронашли смо у вашем сефу $COUNT$ ставки са слабим лозинкама. Требали бисте их ажурирати да би користили јаче лозинке.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Пронађене поновне лозинке" }, - "reusedPasswordsFoundDesc": { - "message": "Нашли смо $COUNT$ лозинке које се поново користе у вашем сефу. Требали бисте да их промените у јединствену вредност.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Портал провајдера" }, + "success": { + "message": "Успех" + }, "viewCollection": { "message": "Преглед колекције" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Приступ налога машине ажуриран" }, - "unassignedItemsBanner": { - "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + "restrictedGroupAccessDesc": { + "message": "Не можете да се додате у групу." }, "unassignedItemsBannerSelfHost": { "message": "Обавештење: 2. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле. Доделите ове ставке колекцији са Админ конзолом да бисте их учинили видљивим." + }, + "unassignedItemsBannerNotice": { + "message": "Напомена: Недодељене ставке организације више нису видљиве у вашем приказу Сви сефови на свим уређајима и сада су доступне само преко Админ конзоле." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Обавештење: 16. маја 2024. недодељене ставке организације више неће бити видљиве у вашем приказу Сви сефови на свим уређајима и биће им доступне само преко Админ конзоле." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Избриши провајдера" + }, + "deleteProviderConfirmation": { + "message": "Брисање провајдера је трајно и неповратно. Унесите своју главну лозинку да бисте потврдили брисање провајдера и свих повезаних података." + }, + "deleteProviderName": { + "message": "Не може да се избрише $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провајдер је избрисан" + }, + "providerDeletedDesc": { + "message": "Провајдер и сви повезани подаци су избрисани." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Захтевали сте брисање овог провајдера. Користите дугме испод да потврдите." + }, + "deleteProviderWarning": { + "message": "Брисање провајдера је трајно. Не може се поништити." + }, + "errorAssigningTargetCollection": { + "message": "Грешка при додељивању циљне колекције." + }, + "errorAssigningTargetFolder": { + "message": "Грешка при додељивању циљне фасцикле." + }, + "integrationsAndSdks": { + "message": "Интеграције & SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Интеграције" + }, + "integrationsDesc": { + "message": "Аутоматски синхронизујте тајне од Bitwarden Secrets Manager са сервисима треће стране." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Употребите Bitwarden Secrets Manager SDK на следећим програмским језицима да направите сопствене апликације." + }, + "setUpGithubActions": { + "message": "Подесити акције GitHub-а" + }, + "setUpGitlabCICD": { + "message": "Подесити GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Подесити Ansible" + }, + "cSharpSDKRepo": { + "message": "Преглед C# спремишта" + }, + "cPlusPlusSDKRepo": { + "message": "Преглед C++ спремишта" + }, + "jsWebAssemblySDKRepo": { + "message": "Преглед JS WebAssembly спремишта" + }, + "javaSDKRepo": { + "message": "Преглед Java спремишта" + }, + "pythonSDKRepo": { + "message": "Преглед Python спремишта" + }, + "phpSDKRepo": { + "message": "Преглед php спремишта" + }, + "rubySDKRepo": { + "message": "Преглед Ruby спремишта" + }, + "goSDKRepo": { + "message": "Преглед Go спремишта" + }, + "createNewClientToManageAsProvider": { + "message": "Креирајте нову клијентску организацију којом ћете управљати као добављач. Додатна места ће се одразити у следећем обрачунском циклусу." + }, + "selectAPlan": { + "message": "Изаберите пакет" + }, + "thirtyFivePercentDiscount": { + "message": "Попуст од 35%" + }, + "monthPerMember": { + "message": "месечно по члану" + }, + "seats": { + "message": "Места" + }, + "addOrganization": { + "message": "Додај организацију" + }, + "createdNewClient": { + "message": "Нови клијент је успешно креиран" + }, + "noAccess": { + "message": "Немате приступ" + }, + "collectionAdminConsoleManaged": { + "message": "Овој колекцији се може приступити само са администраторске конзоле" + }, + "organizationOptionsMenu": { + "message": "Укључи мени Организација" + }, + "vaultItemSelect": { + "message": "Изаберите ставку сефа" + }, + "collectionItemSelect": { + "message": "Изаберите ставку колекције" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sr_CS/messages.json b/apps/web/src/locales/sr_CS/messages.json index 15770437ce..ecc9093748 100644 --- a/apps/web/src/locales/sr_CS/messages.json +++ b/apps/web/src/locales/sr_CS/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/sv/messages.json b/apps/web/src/locales/sv/messages.json index 57321d3376..1d082c5eab 100644 --- a/apps/web/src/locales/sv/messages.json +++ b/apps/web/src/locales/sv/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Osäkra webbplatser hittades" }, - "unsecuredWebsitesFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med osäkra URI:er. Om webbplatsen stödjer det bör du ändra deras URI-protokoll till https://.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Inloggningar utan 2FA hittades" }, - "inactive2faFoundDesc": { - "message": "Vi hittade $COUNT$ webbplats(er) i ditt valv som kanske inte har tvåstegsverifiering konfigurerat (enligt 2fa.directory). För att skydda dessa konton ytterligare bör du aktivera tvåstegsverifiering.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Avslöjade lösenord hittades" }, - "exposedPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med lösenord som har äventyrats i kända dataintrång. Du bör ändra dessa till att använda nya lösenord.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Svaga lösenord hittades" }, - "weakPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ objekt i ditt valv med lösenord som inte är starka. Du bör ändra dessa till att använda starkare lösenord.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Återanvända lösenord hittades" }, - "reusedPasswordsFoundDesc": { - "message": "Vi hittade $COUNT$ lösenord som återanvänds i ditt valv. Du bör ändra dessa till unika lösenord.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Du kan bara tilldela samlingar som du hanterar." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "Visa samling" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "Du kan inte lägga till dig själv i en grupp." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Radera leverantör" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrationer och SDK:er", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrationer" + }, + "integrationsDesc": { + "message": "Synkronisera automatiskt hemligheter från Bitwarden Secrets Manager till en tredjepartstjänst." + }, + "sdks": { + "message": "SDK:er" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Ställ in GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Ställ in Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/te/messages.json b/apps/web/src/locales/te/messages.json index 781c9000cb..417c7adc6a 100644 --- a/apps/web/src/locales/te/messages.json +++ b/apps/web/src/locales/te/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/th/messages.json b/apps/web/src/locales/th/messages.json index 1ecd45a9e4..dd519daa29 100644 --- a/apps/web/src/locales/th/messages.json +++ b/apps/web/src/locales/th/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Unsecured websites found" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Exposed passwords found" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Weak passwords found" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Reused passwords found" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/tr/messages.json b/apps/web/src/locales/tr/messages.json index 65ef93eef7..2954942728 100644 --- a/apps/web/src/locales/tr/messages.json +++ b/apps/web/src/locales/tr/messages.json @@ -657,7 +657,7 @@ "message": "Geçiş anahtarı başarıyla oluşturuldu." }, "customPasskeyNameInfo": { - "message": "Tanımlamanıza yardımcı olması için Passkey'inize bir isim verin." + "message": "Sonradan tanıyabilmeniz için geçiş anahtarınıza bir isim verin." }, "useForVaultEncryption": { "message": "Kasa şifrelemesinde kullan" @@ -678,7 +678,7 @@ "message": "Şifreleme için kullanılır" }, "loginWithPasskeyEnabled": { - "message": "Passkey ile giriş yap" + "message": "Geçiş anahtarıyla giriş açık" }, "passkeySaved": { "message": "$NAME$ kaydedildi", @@ -696,10 +696,10 @@ "message": "Geçiş anahtarını kaldır" }, "removePasskeyInfo": { - "message": "Tüm Passkey'ler kaldırılırsa, ana parolanız olmadan yeni cihazlara giriş yapamazsınız." + "message": "Tüm geçiş anahtarları kaldırılırsa ana parolanız olmadan yeni cihazlara giriş yapamazsınız." }, "passkeyLimitReachedInfo": { - "message": "Passkey sınırına ulaşıldı. Başka bir Passkey eklemek için bir Passley'i kaldırın." + "message": "Geçiş anahtarı sınırına ulaşıldı. Yeni bir geçiş anahtarı eklemek için mevcut bir geçiş anahtarını kaldırın." }, "tryAgain": { "message": "Yeniden dene" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Güvensiz web siteleri bulundu" }, - "unsecuredWebsitesFoundDesc": { - "message": "Kasanızda güvenli olmayan URI'ye sahip $COUNT$ kayıt bulduk. Web sitesi izin veriyorsa URI şemasını https:// olarak değiştirmelisiniz.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "İki aşamalı girişi olmayan hesaplar bulundu" }, - "inactive2faFoundDesc": { - "message": "Kasanızda iki aşamalı giriş kullanmıyor olabilecek $COUNT$ web sitesi bulduk (2fa.directory’ye göre). Bu hesapları daha iyi korumak için iki aşamalı girişi etkinleştirmelisiniz.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Açığa çıkmış parolalar bulundu" }, - "exposedPasswordsFoundDesc": { - "message": "Kasanızda, bilinen veri ihlallerine maruz kalmış parolalara sahip $COUNT$ kayıt bulundu. Bu parolaları değiştirmelisiniz.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Zayıf parolalar pulundu" }, - "weakPasswordsFoundDesc": { - "message": "Kasanızda zayıf parolalara sahip $COUNT$ kayıt bulduk. Bunları güncelleyip daha güçlü parolalar kullanmalısınız.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Yeniden kullanılmış parolalar bulundu" }, - "reusedPasswordsFoundDesc": { - "message": "Kasanızda tekrar kullanılmakta olan $COUNT$ parola bulduk. Onları benzersiz parolalarla değiştirmelisiniz.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -2323,7 +2343,7 @@ "message": "Müşteri hizmetleriyle iletişime geçin" }, "contactSupportShort": { - "message": "Destek Ekibiyle İletişime Geç" + "message": "Destek ekibiyle iletişime geç" }, "updatedPaymentMethod": { "message": "Ödeme yöntemi güncellendi." @@ -2427,7 +2447,7 @@ "message": "İşletmeler ve diğer ekipler için." }, "planNameTeamsStarter": { - "message": "Başlangıç Ekipleri" + "message": "Teams Starter" }, "planNameEnterprise": { "message": "Kurumsal" @@ -3678,7 +3698,7 @@ "message": "Kasa zaman aşımı eyleminin ne zaman gerçekleştirileceğini seçin." }, "vaultTimeoutLogoutDesc": { - "message": "Kasanızın oturumundan ne zaman çıkış yapılacağını seçin." + "message": "Kasanızdan ne zaman çıkış yapılacağını seçin." }, "oneMinute": { "message": "1 dakika" @@ -4782,7 +4802,7 @@ "message": "Ana parolaları olan mevcut hesaplar, yöneticilerin hesaplarını kurtarabilmesi için üyelerin kendi kendilerine kaydolmalarını gerektirecektir. Otomatik kayıt, yeni üyeler için hesap kurtarmayı açacaktır." }, "accountRecoverySingleOrgRequirementDesc": { - "message": "The single organization Enterprise policy must be turned on before activating this policy." + "message": "Bu ilkeyi etkinleştirmeden önce tek kuruluş kurumsal ilkesi etkinleştirilmelidir." }, "resetPasswordPolicyAutoEnroll": { "message": "Otomatik eklenme" @@ -4957,7 +4977,7 @@ "message": "Mevcut kuruluşu ekle" }, "addNewOrganization": { - "message": "Add new organization" + "message": "Yeni kuruluş ekle" }, "myProvider": { "message": "Sağlayıcım" @@ -5796,7 +5816,7 @@ "message": "Cihaz doğrulamasını aç" }, "deviceVerificationDesc": { - "message": "Verification codes are sent to your email address when logging in from an unrecognized device" + "message": "Tanınmayan bir cihazdan oturum açarken e-posta adresinize doğrulama kodları gönderilir" }, "updatedDeviceVerification": { "message": "Cihaz doğrulaması güncellendi" @@ -5940,13 +5960,13 @@ } }, "launchDuoAndFollowStepsToFinishLoggingIn": { - "message": "DUO'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." + "message": "Duo'yu başlatın ve oturum açmayı tamamlamak için adımları izleyin." }, "duoRequiredByOrgForAccount": { - "message": "Hesabınız için DUO iki adımlı giriş gereklidir." + "message": "Hesabınız için Duo iki adımlı giriş gereklidir." }, "launchDuo": { - "message": "DUO'yu başlat" + "message": "Duo'yu başlat" }, "turnOn": { "message": "Aç" @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Kullanıcıları bu gruba ekleyerek koleksiyonlara erişim izni verin." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Mevcut ve gelecekteki tüm koleksiyonlara erişim izni verin." }, @@ -6647,16 +6670,16 @@ } }, "verificationRequiredForActionSetPinToContinue": { - "message": "Verification required for this action. Set a PIN to continue." + "message": "Bu işlem için doğrulama gerekiyor. Devam etmek için bir PIN belirleyin." }, "setPin": { - "message": "Set PIN" + "message": "PIN belirle" }, "verifyWithBiometrics": { - "message": "Verify with biometrics" + "message": "Biyometri ile doğrula" }, "awaitingConfirmation": { - "message": "Awaiting confirmation" + "message": "Onay bekleniyor" }, "couldNotCompleteBiometrics": { "message": "Biyometri işlemi tamamlanamadı." @@ -6671,10 +6694,10 @@ "message": "PIN kullan" }, "useBiometrics": { - "message": "Use biometrics" + "message": "Biyometri kullan" }, "enterVerificationCodeSentToEmail": { - "message": "Enter the verification code that was sent to your email." + "message": "E-posta adresinize gönderilen doğrulama kodunu girin." }, "resendCode": { "message": "Kodu yeniden gönder" @@ -7150,7 +7173,7 @@ } }, "verificationRequired": { - "message": "Verification required", + "message": "Doğrulama gerekli", "description": "Default title for the user verification dialog." }, "recoverAccount": { @@ -7517,11 +7540,11 @@ "message": "Hizmet hesabı erişimi güncellendi" }, "commonImportFormats": { - "message": "Common formats", + "message": "Sık kullanılan biçimler", "description": "Label indicating the most common import formats" }, "maintainYourSubscription": { - "message": "$ORG$ aboneliğinizi sürdürmek için, ", + "message": "$ORG$ aboneliğinizi sürdürmek için ", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'", "placeholders": { "org": { @@ -7531,7 +7554,7 @@ } }, "addAPaymentMethod": { - "message": "bir ödeme yöntemi ekle", + "message": "bir ödeme yöntemi ekleyin", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'To maintain your subscription for $ORG$, add a payment method.'" }, "collectionEnhancementsDesc": { @@ -7554,7 +7577,7 @@ "message": "E-posta adresinize bir onay e-postası gönderdik:" }, "confirmCollectionEnhancementsDialogTitle": { - "message": "Bu eylem geri alınamaz" + "message": "Bu işlem geri alınamaz" }, "confirmCollectionEnhancementsDialogContent": { "message": "Bu özelliğin etkinleştirilmesi, yönetici rolünün kullanımdan kaldırılmasına ve onun yerine Yönetebilir izninin getirilmesine neden olur. Bu birkaç dakika sürecektir. Tamamlanana kadar herhangi bir organizasyon değişikliği yapmayın. Devam etmek istediğinizden emin misiniz?" @@ -7580,11 +7603,11 @@ "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooDifficultToUse": { - "message": "Kullanılması zor", + "message": "Kullanması zor", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "notUsingEnough": { - "message": "Yeterince kullanılmama", + "message": "Fazla kullanmıyorum", "description": "An option for the offboarding survey shown when a user cancels their subscription." }, "tooExpensive": { @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Sağlayıcı Portalı" }, + "success": { + "message": "Başarılı" + }, "viewCollection": { "message": "View collection" }, @@ -7659,10 +7685,10 @@ "message": "Assigned" }, "used": { - "message": "Used" + "message": "Kullanılan" }, "remaining": { - "message": "Remaining" + "message": "Kalan" }, "unlinkOrganization": { "message": "Unlink organization" @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "$ID$ silinemedi", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/uk/messages.json b/apps/web/src/locales/uk/messages.json index 1baaa5c67b..23aa9b94b9 100644 --- a/apps/web/src/locales/uk/messages.json +++ b/apps/web/src/locales/uk/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Знайдено незахищені вебсайти" }, - "unsecuredWebsitesFoundDesc": { - "message": "Ми знайшли $COUNT$ записів у вашому сховищі з незахищеними URL-адресами. Вам слід змінити їхні URL-схеми на https://, якщо вони це дозволяють.", + "unsecuredWebsitesFoundReportDesc": { + "message": "Ми знайшли $COUNT$ записів у вашому $VAULT$ з незахищеними URL-адресами. Вам слід змінити їхні URL-схеми на https://, якщо вони це дозволяють.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Знайдено записи без двоетапної перевірки" }, - "inactive2faFoundDesc": { - "message": "Ми знайшли $COUNT$ вебсайтів у вашому сховищі, що можуть бути не налаштовані для двоетапної перевірки (за даними 2fa.directory). Для захисту цих облікових записів вам слід активувати двоетапну перевірку.", + "inactive2faFoundReportDesc": { + "message": "Ми знайшли $COUNT$ вебсайтів у вашому $VAULT$, що можуть бути не налаштовані для двоетапної перевірки (за даними 2fa.directory). Для захисту цих облікових записів вам слід активувати двоетапну перевірку.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Знайдено викриті паролі" }, - "exposedPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ записів з паролями, які було викрито у відомих витоках даних. Вам слід змінити їх з використанням нового пароля.", + "exposedPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ записів з паролями, які було викрито у відомих витоках даних. Вам слід змінити їх з використанням нового пароля.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Знайдено ненадійні паролі" }, - "weakPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ записів з ненадійними паролями. Вам слід оновити їх з використанням надійніших паролів.", + "weakPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ записів з ненадійними паролями. Вам слід оновити їх з використанням надійніших паролів.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Знайдено повторювані паролі" }, - "reusedPasswordsFoundDesc": { - "message": "У вашому сховищі знайдено $COUNT$ паролів з повторним використанням. Вам слід змінити їх на унікальні.", + "reusedPasswordsFoundReportDesc": { + "message": "У вашому $VAULT$ знайдено $COUNT$ паролів з повторним використанням. Вам слід змінити їх на унікальні.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Надати доступ до збірок, додавши їх до цієї групи." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "Ви можете призначити лише збірки, якими керуєте." + }, "accessAllCollectionsDesc": { "message": "Надати доступ до всіх наявних і майбутніх збірок." }, @@ -7601,11 +7624,14 @@ "message": "Читати блог випусків" }, "adminConsole": { - "message": "Консоль адміністратора" + "message": "консолі адміністратора," }, "providerPortal": { "message": "Портал провайдера" }, + "success": { + "message": "Успішно" + }, "viewCollection": { "message": "Переглянути збірку" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Доступ до машинного облікового запису оновлено" }, - "unassignedItemsBanner": { - "message": "Увага: непризначені елементи організації більше не видимі у поданні \"Усі сховища\" на різних пристроях і тепер доступні лише в консолі адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + "restrictedGroupAccessDesc": { + "message": "Ви не можете додати себе до групи." }, "unassignedItemsBannerSelfHost": { "message": "Сповіщення: 2 травня 2024 року, непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора. Щоб зробити їх видимими, призначте ці елементи збірці в консолі адміністратора." + }, + "unassignedItemsBannerNotice": { + "message": "Примітка: непризначені елементи організації більше не видимі на ваших пристроях у поданні \"Усі сховища\", і тепер доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Примітка: 16 травня 2024 року непризначені елементи організації більше не будуть видимі на ваших пристроях у поданні \"Усі сховища\", і будуть доступні лише через консоль адміністратора." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Призначте ці елементи збірці в", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "щоб зробити їх видимими.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Видалити провайдера" + }, + "deleteProviderConfirmation": { + "message": "Видалення провайдера є остаточною і незворотною дією. Введіть свій головний пароль, щоб підтвердити видалення провайдера і всі пов'язані з ним дані." + }, + "deleteProviderName": { + "message": "Неможливо видалити $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "Перш ніж видалити $ID$, ви повинні від'єднати всіх клієнтів.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Провайдера видалено" + }, + "providerDeletedDesc": { + "message": "Провайдера і всі пов'язані дані було видалено." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "Ви відправили запит на видалення цього провайдера. Натисніть кнопку внизу для підтвердження." + }, + "deleteProviderWarning": { + "message": "Видалення провайдера є незворотною дією. Це не можна буде скасувати." + }, + "errorAssigningTargetCollection": { + "message": "Помилка призначення цільової збірки." + }, + "errorAssigningTargetFolder": { + "message": "Помилка призначення цільової теки." + }, + "integrationsAndSdks": { + "message": "Інтеграції та SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Інтеграції" + }, + "integrationsDesc": { + "message": "Автоматична синхронізація секретів між менеджером секретів Bitwarden і стороннім сервісом." + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "Використовуйте SDK менеджера секретів Bitwarden із зазначеними мовами програмування для створення власних програм." + }, + "setUpGithubActions": { + "message": "Налаштувати дії для Github" + }, + "setUpGitlabCICD": { + "message": "Налаштувати GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Налаштувати Ansible" + }, + "cSharpSDKRepo": { + "message": "Перегляд репозиторію C#" + }, + "cPlusPlusSDKRepo": { + "message": "Перегляд репозиторію C++" + }, + "jsWebAssemblySDKRepo": { + "message": "Перегляд репозиторію JS WebAssembly" + }, + "javaSDKRepo": { + "message": "Перегляд репозиторію Java" + }, + "pythonSDKRepo": { + "message": "Перегляд репозиторію Python" + }, + "phpSDKRepo": { + "message": "Перегляд репозиторію php" + }, + "rubySDKRepo": { + "message": "Перегляд репозиторію Ruby" + }, + "goSDKRepo": { + "message": "Перегляд репозиторію Go" + }, + "createNewClientToManageAsProvider": { + "message": "Створіть нову організацію клієнта, щоб керувати нею як провайдер. Додаткові місця будуть відображені в наступному платіжному циклі." + }, + "selectAPlan": { + "message": "Оберіть тарифний план" + }, + "thirtyFivePercentDiscount": { + "message": "Знижка 35%" + }, + "monthPerMember": { + "message": "на місяць за учасника" + }, + "seats": { + "message": "Місця" + }, + "addOrganization": { + "message": "Додати організацію" + }, + "createdNewClient": { + "message": "Нового клієнта успішно створено" + }, + "noAccess": { + "message": "Немає доступу" + }, + "collectionAdminConsoleManaged": { + "message": "Ця збірка доступна тільки з консолі адміністратора" + }, + "organizationOptionsMenu": { + "message": "Перемкнути меню організації" + }, + "vaultItemSelect": { + "message": "Вибрати елемент сховища" + }, + "collectionItemSelect": { + "message": "Вибрати елемент збірки" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Керування рахунками на порталі провайдера" } } diff --git a/apps/web/src/locales/vi/messages.json b/apps/web/src/locales/vi/messages.json index ffbd72ce9d..aa5ad2c9d3 100644 --- a/apps/web/src/locales/vi/messages.json +++ b/apps/web/src/locales/vi/messages.json @@ -579,7 +579,7 @@ "message": "Quyền truy cập" }, "accessLevel": { - "message": "Access level" + "message": "Cấp độ truy cập" }, "loggedOut": { "message": "Đã đăng xuất" @@ -609,7 +609,7 @@ "message": "Đăng nhập bằng thiết bị" }, "loginWithDeviceEnabledNote": { - "message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?" + "message": "Đăng nhập bằng thiết bị phải được thiết lập trong cài đặt của ứng dụng Bitwarden. Dùng cách khác?" }, "loginWithMasterPassword": { "message": "Đăng nhập bằng mật khẩu chính" @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "Tìm thấy trang web không an toàn" }, - "unsecuredWebsitesFoundDesc": { - "message": "We found $COUNT$ items in your vault with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "Logins without two-step login found" }, - "inactive2faFoundDesc": { - "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "Phát hiện mật khẩu bị rò rĩ" }, - "exposedPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "Phát hiện mật khẩu yếu" }, - "weakPasswordsFoundDesc": { - "message": "We found $COUNT$ items in your vault with passwords that are not strong. You should update them to use stronger passwords.", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "Phát hiện mật khẩu bị trùng" }, - "reusedPasswordsFoundDesc": { - "message": "We found $COUNT$ passwords that are being reused in your vault. You should change them to a unique value.", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "Grant access to collections by adding them to this group." }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "Grant access to all current and future collections." }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/locales/zh_CN/messages.json b/apps/web/src/locales/zh_CN/messages.json index e584785a5a..8e5a9e696b 100644 --- a/apps/web/src/locales/zh_CN/messages.json +++ b/apps/web/src/locales/zh_CN/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "发现不安全的网站" }, - "unsecuredWebsitesFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个项目带有不安全的 URI。如果网站允许,您应该将他们更改为 https://。", + "unsecuredWebsitesFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个带不安全 URI 的项目。如果网站允许,您应将其 URI 方案更改为 https://。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "发现未启用两步登录的登录项目" }, - "inactive2faFoundDesc": { - "message": "我们在您的密码库中发现 $COUNT$ 个网站可能没有配置两步登录(根据 2fa.directory)。为了进一步保护这些账户,您应该设置两步登录。", + "inactive2faFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个网站可能未配置两步登录(通过 2fa.directory)。为进一步保护这些账户,您应设置两步登录。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "发现暴露的密码" }, - "exposedPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个项目的密码在已知数据泄露事件中被暴露。您应该将它们更改为新密码。", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "发现弱密码" }, - "weakPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个弱密码项目。您应该将它们改为更强的密码。", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "发现重复使用的密码" }, - "reusedPasswordsFoundDesc": { - "message": "我们在您的密码库中发现了 $COUNT$ 个被重复使用的密码。您应该将它们更改为唯一值。", + "reusedPasswordsFoundReportDesc": { + "message": "我们在您的 $VAULT$ 中发现了 $COUNT$ 个被重复使用的密码。您应将其更改为唯一值。", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "通过将集合添加到此群组来授予对集合的访问权限。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "您只能分配您管理的集合。" + }, "accessAllCollectionsDesc": { "message": "授予对所有当前和未来的集合的访问权限。" }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "提供商门户" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "查看集合" }, @@ -7698,7 +7724,7 @@ "description": "The date header used when a subscription is past due." }, "pastDueWarningForChargeAutomatically": { - "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从您的订阅到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7712,7 +7738,7 @@ "description": "A warning shown to the user when their subscription is past due and they are charged automatically." }, "pastDueWarningForSendInvoice": { - "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订购。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", + "message": "从第一笔未支付的账单到期之日起,您有 $DAYS$ 天的宽限期来保留您的订阅。请在 $SUSPENSION_DATE$ 之前处理逾期未支付的账单。", "placeholders": { "days": { "content": "$1", @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "机器账户访问权限已更新" }, - "unassignedItemsBanner": { - "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + "restrictedGroupAccessDesc": { + "message": "您不能将自己添加到群组。" }, "unassignedItemsBannerSelfHost": { "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。通过管理控制台将这些项目分配给集合以使其可见。" + }, + "unassignedItemsBannerNotice": { + "message": "注意:未分配的组织项目在您所有设备的「所有密码库」视图中不再可见,现在只能通过管理控制台访问。" + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "注意:从 2024 年 5 月 2 日起,未分配的组织项目在您所有设备的「所有密码库」视图中将不再可见,只能通过管理控制台访问。" + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "以使其可见。", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "删除提供商" + }, + "deleteProviderConfirmation": { + "message": "删除提供商是永久性操作,无法撤销!输入您的主密码以确认删除提供商及所有关联的数据。" + }, + "deleteProviderName": { + "message": "无法删除 $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "删除 $ID$ 之前,您必须取消链接所有的客户端。", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "提供商已删除" + }, + "providerDeletedDesc": { + "message": "提供商和所有关联数据已被删除。" + }, + "deleteProviderRecoverConfirmDesc": { + "message": "您已请求删除此提供商。请使用下面的按钮确认。" + }, + "deleteProviderWarning": { + "message": "删除您的提供商是永久性操作,无法撤销!" + }, + "errorAssigningTargetCollection": { + "message": "分配目标集合时出错。" + }, + "errorAssigningTargetFolder": { + "message": "分配目标文件夹时出错。" + }, + "integrationsAndSdks": { + "message": "集成和 SDK", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "集成" + }, + "integrationsDesc": { + "message": "自动将机密从 Bitwarden 机密管理器同步到第三方服务。" + }, + "sdks": { + "message": "SDK" + }, + "sdksDesc": { + "message": "使用以下编程语言的 Bitwarden 机密管理器 SDK 来构建您自己的应用程序。" + }, + "setUpGithubActions": { + "message": "设置 Github Actions" + }, + "setUpGitlabCICD": { + "message": "设置 GitLab CI/CD" + }, + "setUpAnsible": { + "message": "设置 Ansible" + }, + "cSharpSDKRepo": { + "message": "查看 C# 存储库" + }, + "cPlusPlusSDKRepo": { + "message": "查看 C++ 存储库" + }, + "jsWebAssemblySDKRepo": { + "message": "查看 JS WebAssembly 存储库" + }, + "javaSDKRepo": { + "message": "查看 Java 存储库" + }, + "pythonSDKRepo": { + "message": "查看 Python 存储库" + }, + "phpSDKRepo": { + "message": "查看 php 存储库" + }, + "rubySDKRepo": { + "message": "查看 Ruby 存储库" + }, + "goSDKRepo": { + "message": "查看 Go 存储库" + }, + "createNewClientToManageAsProvider": { + "message": "创建一个新的客户组织作为提供商来管理。附加席位将反映在下一个计费周期中。" + }, + "selectAPlan": { + "message": "选择套餐" + }, + "thirtyFivePercentDiscount": { + "message": "35% 折扣" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "席位" + }, + "addOrganization": { + "message": "添加组织" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "暂无权限" + }, + "collectionAdminConsoleManaged": { + "message": "此集合只能从管理控制台访问" + }, + "organizationOptionsMenu": { + "message": "切换组织菜单" + }, + "vaultItemSelect": { + "message": "选择密码库项目" + }, + "collectionItemSelect": { + "message": "选择集合项目" + }, + "manageBillingFromProviderPortalMessage": { + "message": "在供应商门户中管理账单" } } diff --git a/apps/web/src/locales/zh_TW/messages.json b/apps/web/src/locales/zh_TW/messages.json index 89bb26cacd..c2b0060ac6 100644 --- a/apps/web/src/locales/zh_TW/messages.json +++ b/apps/web/src/locales/zh_TW/messages.json @@ -1809,12 +1809,16 @@ "unsecuredWebsitesFound": { "message": "發現不安全的網站" }, - "unsecuredWebsitesFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個使用不安全 URI 的項目。若網站允許,您應變更其網址配置為 https://。", + "unsecuredWebsitesFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with unsecured URIs. You should change their URI scheme to https:// if the website allows it.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1830,12 +1834,16 @@ "inactive2faFound": { "message": "發現未啟用 2FA 的登入" }, - "inactive2faFoundDesc": { - "message": "我們在您的密碼庫中找到 $COUNT$ 個可能未設定兩步驟登入的網站(依據 twofactorauth.org)。若要進一步保護這些帳戶,您應啟用兩步驟登入。", + "inactive2faFoundReportDesc": { + "message": "We found $COUNT$ website(s) in your $VAULT$ that may not be configured with two-step login (according to 2fa.directory). To further protect these accounts, you should set up two-step login.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1854,12 +1862,16 @@ "exposedPasswordsFound": { "message": "發現暴露的密碼" }, - "exposedPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個項目的密碼在已知資料外洩事件中被暴露。您應將它們變更為新密碼。", + "exposedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ that have passwords that were exposed in known data breaches. You should change them to use a new password.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1887,12 +1899,16 @@ "weakPasswordsFound": { "message": "發現弱式密碼" }, - "weakPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 個使用弱式密碼的項目。您應該將它們變更為更強的密碼。", + "weakPasswordsFoundReportDesc": { + "message": "We found $COUNT$ items in your $VAULT$ with passwords that are not strong. You should update them to use stronger passwords.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -1908,12 +1924,16 @@ "reusedPasswordsFound": { "message": "發現重複使用的密碼" }, - "reusedPasswordsFoundDesc": { - "message": "我們在您的密碼庫中找到了 $COUNT$ 組密碼重複使用。您應該將它們變更為不同的密碼。", + "reusedPasswordsFoundReportDesc": { + "message": "We found $COUNT$ passwords that are being reused in your $VAULT$. You should change them to a unique value.", "placeholders": { "count": { "content": "$1", "example": "8" + }, + "vault": { + "content": "$2", + "example": "this will be 'vault' or 'vaults'" } } }, @@ -6460,6 +6480,9 @@ "editGroupCollectionsDesc": { "message": "透過將他們添加到此群組,授予對集合的存取權限。" }, + "editGroupCollectionsRestrictionsDesc": { + "message": "You can only assign collections you manage." + }, "accessAllCollectionsDesc": { "message": "授予對所有目前和未來的集合的存取權限。" }, @@ -7606,6 +7629,9 @@ "providerPortal": { "message": "Provider Portal" }, + "success": { + "message": "Success" + }, "viewCollection": { "message": "View collection" }, @@ -7900,10 +7926,154 @@ "machineAccountAccessUpdated": { "message": "Machine account access updated" }, - "unassignedItemsBanner": { - "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + "restrictedGroupAccessDesc": { + "message": "You cannot add yourself to a group." }, "unassignedItemsBannerSelfHost": { "message": "Notice: On May 2, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console. Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerNotice": { + "message": "Notice: Unassigned organization items are no longer visible in your All Vaults view across devices and are now only accessible via the Admin Console." + }, + "unassignedItemsBannerSelfHostNotice": { + "message": "Notice: On May 16, 2024, unassigned organization items will no longer be visible in your All Vaults view across devices and will only be accessible via the Admin Console." + }, + "unassignedItemsBannerCTAPartOne": { + "message": "Assign these items to a collection from the", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "unassignedItemsBannerCTAPartTwo": { + "message": "to make them visible.", + "description": "This will be part of a larger sentence, which will read like so: Assign these items to a collection from the Admin Console to make them visible." + }, + "deleteProvider": { + "message": "Delete provider" + }, + "deleteProviderConfirmation": { + "message": "Deleting a provider is permanent and irreversible. Enter your master password to confirm the deletion of the provider and all associated data." + }, + "deleteProviderName": { + "message": "Cannot delete $ID$", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "deleteProviderWarningDescription": { + "message": "You must unlink all clients before you can delete $ID$.", + "placeholders": { + "id": { + "content": "$1", + "example": "John Smith" + } + } + }, + "providerDeleted": { + "message": "Provider deleted" + }, + "providerDeletedDesc": { + "message": "The Provider and all associated data has been deleted." + }, + "deleteProviderRecoverConfirmDesc": { + "message": "You have requested to delete this Provider. Use the button below to confirm." + }, + "deleteProviderWarning": { + "message": "Deleting your provider is permanent. It cannot be undone." + }, + "errorAssigningTargetCollection": { + "message": "Error assigning target collection." + }, + "errorAssigningTargetFolder": { + "message": "Error assigning target folder." + }, + "integrationsAndSdks": { + "message": "Integrations & SDKs", + "description": "The title for the section that deals with integrations and SDKs." + }, + "integrations": { + "message": "Integrations" + }, + "integrationsDesc": { + "message": "Automatically sync secrets from Bitwarden Secrets Manager to a third-party service." + }, + "sdks": { + "message": "SDKs" + }, + "sdksDesc": { + "message": "Use Bitwarden Secrets Manager SDK in the following programming languages to build your own applications." + }, + "setUpGithubActions": { + "message": "Set up Github Actions" + }, + "setUpGitlabCICD": { + "message": "Set up GitLab CI/CD" + }, + "setUpAnsible": { + "message": "Set up Ansible" + }, + "cSharpSDKRepo": { + "message": "View C# repository" + }, + "cPlusPlusSDKRepo": { + "message": "View C++ repository" + }, + "jsWebAssemblySDKRepo": { + "message": "View JS WebAssembly repository" + }, + "javaSDKRepo": { + "message": "View Java repository" + }, + "pythonSDKRepo": { + "message": "View Python repository" + }, + "phpSDKRepo": { + "message": "View php repository" + }, + "rubySDKRepo": { + "message": "View Ruby repository" + }, + "goSDKRepo": { + "message": "View Go repository" + }, + "createNewClientToManageAsProvider": { + "message": "Create a new client organization to manage as a Provider. Additional seats will be reflected in the next billing cycle." + }, + "selectAPlan": { + "message": "Select a plan" + }, + "thirtyFivePercentDiscount": { + "message": "35% Discount" + }, + "monthPerMember": { + "message": "month per member" + }, + "seats": { + "message": "Seats" + }, + "addOrganization": { + "message": "Add organization" + }, + "createdNewClient": { + "message": "Successfully created new client" + }, + "noAccess": { + "message": "No access" + }, + "collectionAdminConsoleManaged": { + "message": "This collection is only accessible from the admin console" + }, + "organizationOptionsMenu": { + "message": "Toggle Organization Menu" + }, + "vaultItemSelect": { + "message": "Select vault item" + }, + "collectionItemSelect": { + "message": "Select collection item" + }, + "manageBillingFromProviderPortalMessage": { + "message": "Manage billing from the Provider Portal" } } diff --git a/apps/web/src/scss/styles.scss b/apps/web/src/scss/styles.scss index 98b3512ba5..8fbea200a9 100644 --- a/apps/web/src/scss/styles.scss +++ b/apps/web/src/scss/styles.scss @@ -43,8 +43,6 @@ @import "~bootstrap/scss/_utilities"; @import "~bootstrap/scss/_print"; -@import "~ngx-toastr/toastr"; - @import "./base"; @import "./buttons"; @import "./callouts"; @@ -54,5 +52,4 @@ @import "./pages"; @import "./plugins"; @import "./tables"; -@import "./toasts"; @import "./vault-filters"; diff --git a/apps/web/src/scss/toasts.scss b/apps/web/src/scss/toasts.scss deleted file mode 100644 index 6685de6449..0000000000 --- a/apps/web/src/scss/toasts.scss +++ /dev/null @@ -1,117 +0,0 @@ -.toast-container { - .toast-close-button { - font-size: 18px; - margin-right: 4px; - } - - .ngx-toastr { - align-items: center; - background-image: none !important; - border-radius: $border-radius; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.35); - display: flex; - padding: 15px; - - .toast-close-button { - position: absolute; - right: 5px; - top: 0; - } - - &:hover { - box-shadow: 0 0 10px rgba(0, 0, 0, 0.6); - } - - .icon i::before { - float: left; - font-style: normal; - font-family: $icomoon-font-family; - font-size: 25px; - line-height: 20px; - padding-right: 15px; - } - - .toast-message { - p { - margin-bottom: 0.5rem; - - &:last-child { - margin-bottom: 0; - } - } - } - - &.toast-danger, - &.toast-error { - @include themify($themes) { - background-color: themed("danger"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textDangerColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "error"); - } - } - - &.toast-warning { - @include themify($themes) { - background-color: themed("warning"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textWarningColor") !important; - } - } - - .icon i::before { - content: map_get($icons, "exclamation-triangle"); - } - } - - &.toast-info { - @include themify($themes) { - background-color: themed("info"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textInfoColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "info-circle"); - } - } - - &.toast-success { - @include themify($themes) { - background-color: themed("success"); - } - - &, - &:before, - & .toast-close-button { - @include themify($themes) { - color: themed("textSuccessColor") !important; - } - } - - .icon i:before { - content: map_get($icons, "check"); - } - } - } -} diff --git a/apps/web/tailwind.config.js b/apps/web/tailwind.config.js index a944f9dd67..e80bf6a834 100644 --- a/apps/web/tailwind.config.js +++ b/apps/web/tailwind.config.js @@ -4,6 +4,7 @@ const config = require("../../libs/components/tailwind.config.base"); config.content = [ "./src/**/*.{html,ts}", "../../libs/components/src/**/*.{html,ts}", + "../../libs/auth/src/**/*.{html,ts}", "../../bitwarden_license/bit-web/src/**/*.{html,ts}", ]; diff --git a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html index 9f08e98dae..94f2e8a422 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/policies/activate-autofill.component.html @@ -5,15 +5,7 @@ }} -
-
- - -
-
+ + + {{ "turnOn" | i18n }} + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts new file mode 100644 index 0000000000..604d61f3db --- /dev/null +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/base-clients.component.ts @@ -0,0 +1,130 @@ +import { SelectionModel } from "@angular/cdk/collections"; +import { Directive, OnDestroy, OnInit } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { BehaviorSubject, from, Subject, switchMap } from "rxjs"; +import { first, takeUntil } from "rxjs/operators"; + +import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { WebProviderService } from "../services/web-provider.service"; + +@Directive() +export abstract class BaseClientsComponent implements OnInit, OnDestroy { + protected destroy$ = new Subject(); + + private searchText$ = new BehaviorSubject(""); + + get searchText() { + return this.searchText$.value; + } + + set searchText(value: string) { + this.searchText$.next(value); + this.selection.clear(); + this.dataSource.filter = value; + } + + private searching = false; + protected scrolled = false; + protected pageSize = 100; + private pagedClientsCount = 0; + protected selection = new SelectionModel(true, []); + + protected clients: ProviderOrganizationOrganizationDetailsResponse[]; + protected pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; + protected dataSource = new TableDataSource(); + + abstract providerId: string; + + protected constructor( + protected activatedRoute: ActivatedRoute, + protected dialogService: DialogService, + private i18nService: I18nService, + private searchService: SearchService, + private toastService: ToastService, + private validationService: ValidationService, + private webProviderService: WebProviderService, + ) {} + + abstract load(): Promise; + + ngOnInit() { + this.activatedRoute.queryParams + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((queryParams) => { + this.searchText = queryParams.search; + }); + + this.searchText$ + .pipe( + switchMap((searchText) => from(this.searchService.isSearchable(searchText))), + takeUntil(this.destroy$), + ) + .subscribe((isSearchable) => { + this.searching = isSearchable; + }); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + isPaging() { + if (this.searching && this.scrolled) { + this.resetPaging(); + } + return !this.searching && this.clients && this.clients.length > this.pageSize; + } + + resetPaging() { + this.pagedClients = []; + this.loadMore(); + } + + loadMore() { + if (!this.clients || this.clients.length <= this.pageSize) { + return; + } + const pagedLength = this.pagedClients.length; + let pagedSize = this.pageSize; + if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { + pagedSize = this.pagedClientsCount; + } + if (this.clients.length > pagedLength) { + this.pagedClients = this.pagedClients.concat( + this.clients.slice(pagedLength, pagedLength + pagedSize), + ); + } + this.pagedClientsCount = this.pagedClients.length; + this.scrolled = this.pagedClients.length > this.pageSize; + } + + async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: organization.organizationName, + content: { key: "detachOrganizationConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.webProviderService.detachOrganization(this.providerId, organization.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("detachedOrganization", organization.organizationName), + }); + await this.load(); + } catch (e) { + this.validationService.showError(e); + } + } +} diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts index abdfd6deff..7a96bdc7c7 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/clients/clients.component.ts @@ -1,9 +1,8 @@ -import { Component, OnInit } from "@angular/core"; +import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs"; -import { first, switchMap, takeUntil } from "rxjs/operators"; +import { firstValueFrom, from, map } from "rxjs"; +import { switchMap, takeUntil } from "rxjs/operators"; -import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; @@ -11,19 +10,17 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response"; +import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction"; import { PlanType } from "@bitwarden/common/billing/enums"; -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 { 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 { DialogService } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { WebProviderService } from "../services/web-provider.service"; import { AddOrganizationComponent } from "./add-organization.component"; +import { BaseClientsComponent } from "./base-clients.component"; const DisallowedPlanTypes = [ PlanType.Free, @@ -36,90 +33,66 @@ const DisallowedPlanTypes = [ @Component({ templateUrl: "clients.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ClientsComponent implements OnInit { +export class ClientsComponent extends BaseClientsComponent { providerId: string; addableOrganizations: Organization[]; loading = true; manageOrganizations = false; showAddExisting = false; - clients: ProviderOrganizationOrganizationDetailsResponse[]; - pagedClients: ProviderOrganizationOrganizationDetailsResponse[]; - - protected didScroll = false; - protected pageSize = 100; - protected actionPromise: Promise; - private pagedClientsCount = 0; - - protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableConsolidatedBilling, - false, - ); - private destroy$ = new Subject(); - private _searchText$ = new BehaviorSubject(""); - private isSearching: boolean = false; - - get searchText() { - return this._searchText$.value; - } - - set searchText(value: string) { - this._searchText$.next(value); - } - constructor( - private route: ActivatedRoute, private router: Router, private providerService: ProviderService, private apiService: ApiService, - private searchService: SearchService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private validationService: ValidationService, - private webProviderService: WebProviderService, - private logService: LogService, - private modalService: ModalService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, - private dialogService: DialogService, private configService: ConfigService, - ) {} + activatedRoute: ActivatedRoute, + dialogService: DialogService, + i18nService: I18nService, + searchService: SearchService, + toastService: ToastService, + validationService: ValidationService, + webProviderService: WebProviderService, + ) { + super( + activatedRoute, + dialogService, + i18nService, + searchService, + toastService, + validationService, + webProviderService, + ); + } - async ngOnInit() { - const enableConsolidatedBilling = await firstValueFrom(this.enableConsolidatedBilling$); - - if (enableConsolidatedBilling) { - await this.router.navigate(["../manage-client-organizations"], { relativeTo: this.route }); - } else { - this.route.parent.params - .pipe( - switchMap((params) => { - this.providerId = params.providerId; - return from(this.load()); - }), - takeUntil(this.destroy$), - ) - .subscribe(); - - this.route.queryParams.pipe(first(), takeUntil(this.destroy$)).subscribe((qParams) => { - this.searchText = qParams.search; - }); - - this._searchText$ - .pipe( - switchMap((searchText) => from(this.searchService.isSearchable(searchText))), - takeUntil(this.destroy$), - ) - .subscribe((isSearchable) => { - this.isSearching = isSearchable; - }); - } + ngOnInit() { + this.activatedRoute.parent.params + .pipe( + switchMap((params) => { + this.providerId = params.providerId; + return this.providerService.get$(this.providerId).pipe( + canAccessBilling(this.configService), + map((canAccessBilling) => { + if (canAccessBilling) { + return from( + this.router.navigate(["../manage-client-organizations"], { + relativeTo: this.activatedRoute, + }), + ); + } else { + return from(this.load()); + } + }), + ); + }), + takeUntil(this.destroy$), + ) + .subscribe(); } ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); + super.ngOnDestroy(); } async load() { @@ -141,37 +114,6 @@ export class ClientsComponent implements OnInit { this.loading = false; } - isPaging() { - const searching = this.isSearching; - if (searching && this.didScroll) { - this.resetPaging(); - } - return !searching && this.clients && this.clients.length > this.pageSize; - } - - resetPaging() { - this.pagedClients = []; - this.loadMore(); - } - - loadMore() { - if (!this.clients || this.clients.length <= this.pageSize) { - return; - } - const pagedLength = this.pagedClients.length; - let pagedSize = this.pageSize; - if (pagedLength === 0 && this.pagedClientsCount > this.pageSize) { - pagedSize = this.pagedClientsCount; - } - if (this.clients.length > pagedLength) { - this.pagedClients = this.pagedClients.concat( - this.clients.slice(pagedLength, pagedLength + pagedSize), - ); - } - this.pagedClientsCount = this.pagedClients.length; - this.didScroll = this.pagedClients.length > this.pageSize; - } - async addExistingOrganization() { const dialogRef = AddOrganizationComponent.open(this.dialogService, { providerId: this.providerId, @@ -182,33 +124,4 @@ export class ClientsComponent implements OnInit { await this.load(); } } - - async remove(organization: ProviderOrganizationOrganizationDetailsResponse) { - const confirmed = await this.dialogService.openSimpleDialog({ - title: organization.organizationName, - content: { key: "detachOrganizationConfirmation" }, - type: "warning", - }); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.webProviderService.detachOrganization( - this.providerId, - organization.id, - ); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("detachedOrganization", organization.organizationName), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } } diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html index ca2b1a3545..a1cf2cc5aa 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/providers-layout.component.html @@ -1,5 +1,5 @@ -
+ - - - - - - - - - - - - - - - - - - - - -
- {{ emptyMessage }} -
-
- - -
- -
-
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts deleted file mode 100644 index 8deed43cd3..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; -import { - combineLatest, - firstValueFrom, - map, - Observable, - share, - Subject, - switchMap, - tap, -} from "rxjs"; - -import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view"; - -import { BaseAccessPolicyView } from "../../models/view/access-policy.view"; - -import { AccessPolicyService } from "./access-policy.service"; - -export type AccessSelectorRowView = { - type: "user" | "group" | "serviceAccount" | "project"; - name: string; - id: string; - accessPolicyId: string; - read: boolean; - write: boolean; - icon: string; - userId?: string; - currentUserInGroup?: boolean; - static?: boolean; -}; - -@Component({ - selector: "sm-access-selector", - templateUrl: "./access-selector.component.html", -}) -export class AccessSelectorComponent implements OnInit { - static readonly userIcon = "bwi-user"; - static readonly groupIcon = "bwi-family"; - static readonly serviceAccountIcon = "bwi-wrench"; - static readonly projectIcon = "bwi-collection"; - - /** - * Emits the selected items on submit. - */ - @Output() onCreateAccessPolicies = new EventEmitter(); - @Output() onDeleteAccessPolicy = new EventEmitter(); - @Output() onUpdateAccessPolicy = new EventEmitter(); - - @Input() label: string; - @Input() hint: string; - @Input() columnTitle: string; - @Input() emptyMessage: string; - @Input() granteeType: "people" | "serviceAccounts" | "projects"; - - protected rows$ = new Subject(); - @Input() private set rows(value: AccessSelectorRowView[]) { - const sorted = value.sort((a, b) => { - if (a.icon == b.icon) { - return a.name.localeCompare(b.name); - } - if (a.icon == AccessSelectorComponent.userIcon) { - return -1; - } - return 1; - }); - this.rows$.next(sorted); - } - - private maxLength = 15; - protected formGroup = new FormGroup({ - multiSelect: new FormControl([], [Validators.required, Validators.maxLength(this.maxLength)]), - }); - protected loading = true; - - protected selectItems$: Observable = combineLatest([ - this.rows$, - this.route.params, - ]).pipe( - switchMap(([rows, params]) => - this.getPotentialGrantees(params.organizationId).then((grantees) => - grantees - .filter((g) => !rows.some((row) => row.id === g.id)) - .map((granteeView) => { - let icon: string; - let listName = granteeView.name; - let labelName = granteeView.name; - if (granteeView.type === "user") { - icon = AccessSelectorComponent.userIcon; - if (Utils.isNullOrWhitespace(granteeView.name)) { - listName = granteeView.email; - labelName = granteeView.email; - } else { - listName = `${granteeView.name} (${granteeView.email})`; - } - } else if (granteeView.type === "group") { - icon = AccessSelectorComponent.groupIcon; - } else if (granteeView.type === "serviceAccount") { - icon = AccessSelectorComponent.serviceAccountIcon; - } else if (granteeView.type === "project") { - icon = AccessSelectorComponent.projectIcon; - } - return { - icon: icon, - id: granteeView.id, - labelName: labelName, - listName: listName, - }; - }), - ), - ), - map((selectItems) => selectItems.sort((a, b) => a.listName.localeCompare(b.listName))), - tap(() => { - this.loading = false; - this.formGroup.reset(); - this.formGroup.enable(); - }), - share(), - ); - - constructor( - private accessPolicyService: AccessPolicyService, - private route: ActivatedRoute, - ) {} - - ngOnInit(): void { - this.formGroup.disable(); - } - - submit = async () => { - this.formGroup.markAllAsTouched(); - if (this.formGroup.invalid) { - return; - } - this.formGroup.disable(); - this.loading = true; - - this.onCreateAccessPolicies.emit(this.formGroup.value.multiSelect); - - return firstValueFrom(this.selectItems$); - }; - - async update(target: any, row: AccessSelectorRowView): Promise { - if (target.value === "canRead") { - row.read = true; - row.write = false; - } else if (target.value === "canReadWrite") { - row.read = true; - row.write = true; - } - this.onUpdateAccessPolicy.emit(row); - } - - delete = (row: AccessSelectorRowView) => async () => { - this.loading = true; - this.formGroup.disable(); - this.onDeleteAccessPolicy.emit(row); - return firstValueFrom(this.selectItems$); - }; - - private getPotentialGrantees(organizationId: string) { - switch (this.granteeType) { - case "people": - return this.accessPolicyService.getPeoplePotentialGrantees(organizationId); - case "serviceAccounts": - return this.accessPolicyService.getServiceAccountsPotentialGrantees(organizationId); - case "projects": - return this.accessPolicyService.getProjectsPotentialGrantees(organizationId); - } - } - - static getAccessItemType(item: SelectItemView) { - switch (item.icon) { - case AccessSelectorComponent.userIcon: - return "user"; - case AccessSelectorComponent.groupIcon: - return "group"; - case AccessSelectorComponent.serviceAccountIcon: - return "serviceAccount"; - case AccessSelectorComponent.projectIcon: - return "project"; - } - } - - static getBaseAccessPolicyView(row: AccessSelectorRowView) { - const view = new BaseAccessPolicyView(); - view.id = row.accessPolicyId; - view.read = row.read; - view.write = row.write; - return view; - } -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts deleted file mode 100644 index ff391ecacd..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policies-create.request.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { AccessPolicyRequest } from "./access-policy.request"; - -export class AccessPoliciesCreateRequest { - userAccessPolicyRequests?: AccessPolicyRequest[]; - groupAccessPolicyRequests?: AccessPolicyRequest[]; - serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts deleted file mode 100644 index 5aff186e12..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/access-policy-update.request.ts +++ /dev/null @@ -1,4 +0,0 @@ -export class AccessPolicyUpdateRequest { - read: boolean; - write: boolean; -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts new file mode 100644 index 0000000000..e287775cd3 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/project-service-accounts-access-policies.request.ts @@ -0,0 +1,5 @@ +import { AccessPolicyRequest } from "./access-policy.request"; + +export class ProjectServiceAccountsAccessPoliciesRequest { + serviceAccountAccessPolicyRequests?: AccessPolicyRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts new file mode 100644 index 0000000000..74ec52021f --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/requests/service-account-granted-policies.request.ts @@ -0,0 +1,5 @@ +import { GrantedPolicyRequest } from "./granted-policy.request"; + +export class ServiceAccountGrantedPoliciesRequest { + projectGrantedPolicyRequests?: GrantedPolicyRequest[]; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts deleted file mode 100644 index 66d76c1493..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-access-policies.response.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseResponse } from "@bitwarden/common/models/response/base.response"; - -import { - GroupProjectAccessPolicyResponse, - ServiceAccountProjectAccessPolicyResponse, - UserProjectAccessPolicyResponse, -} from "./access-policy.response"; - -export class ProjectAccessPoliciesResponse extends BaseResponse { - userAccessPolicies: UserProjectAccessPolicyResponse[]; - groupAccessPolicies: GroupProjectAccessPolicyResponse[]; - serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; - - constructor(response: any) { - super(response); - const userAccessPolicies = this.getResponseProperty("UserAccessPolicies"); - this.userAccessPolicies = userAccessPolicies.map( - (k: any) => new UserProjectAccessPolicyResponse(k), - ); - const groupAccessPolicies = this.getResponseProperty("GroupAccessPolicies"); - this.groupAccessPolicies = groupAccessPolicies.map( - (k: any) => new GroupProjectAccessPolicyResponse(k), - ); - const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); - this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( - (k: any) => new ServiceAccountProjectAccessPolicyResponse(k), - ); - } -} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts new file mode 100644 index 0000000000..f26a9996dd --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/project-service-accounts-access-policies.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; + +export class ProjectServiceAccountsAccessPoliciesResponse extends BaseResponse { + serviceAccountAccessPolicies: ServiceAccountProjectAccessPolicyResponse[]; + + constructor(response: any) { + super(response); + const serviceAccountAccessPolicies = this.getResponseProperty("ServiceAccountAccessPolicies"); + this.serviceAccountAccessPolicies = serviceAccountAccessPolicies.map( + (k: any) => new ServiceAccountProjectAccessPolicyResponse(k), + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts new file mode 100644 index 0000000000..858a59ff43 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-granted-policies-permission-details.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./service-account-project-policy-permission-details.response"; + +export class ServiceAccountGrantedPoliciesPermissionDetailsResponse extends BaseResponse { + grantedProjectPolicies: ServiceAccountProjectPolicyPermissionDetailsResponse[]; + + constructor(response: any) { + super(response); + const grantedProjectPolicies = this.getResponseProperty("GrantedProjectPolicies"); + this.grantedProjectPolicies = grantedProjectPolicies.map( + (k: any) => new ServiceAccountProjectPolicyPermissionDetailsResponse(k), + ); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts new file mode 100644 index 0000000000..dbc4fe0727 --- /dev/null +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/models/responses/service-account-project-policy-permission-details.response.ts @@ -0,0 +1,14 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +import { ServiceAccountProjectAccessPolicyResponse } from "./access-policy.response"; + +export class ServiceAccountProjectPolicyPermissionDetailsResponse extends BaseResponse { + accessPolicy: ServiceAccountProjectAccessPolicyResponse; + hasPermission: boolean; + + constructor(response: any) { + super(response); + this.accessPolicy = this.getResponseProperty("AccessPolicy"); + this.hasPermission = this.getResponseProperty("HasPermission"); + } +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts index cb302fb7db..cb723af6d7 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/sm-shared.module.ts @@ -13,7 +13,6 @@ import { ProductSwitcherModule } from "@bitwarden/web-vault/app/layouts/product- import { SharedModule } from "@bitwarden/web-vault/app/shared"; import { AccessPolicySelectorComponent } from "./access-policies/access-policy-selector/access-policy-selector.component"; -import { AccessSelectorComponent } from "./access-policies/access-selector.component"; import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component"; import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component"; import { NewMenuComponent } from "./new-menu.component"; @@ -35,7 +34,6 @@ import { SecretsListComponent } from "./secrets-list.component"; ], exports: [ AccessPolicySelectorComponent, - AccessSelectorComponent, BulkConfirmationDialogComponent, BulkStatusDialogComponent, HeaderModule, @@ -49,7 +47,6 @@ import { SecretsListComponent } from "./secrets-list.component"; ], declarations: [ AccessPolicySelectorComponent, - AccessSelectorComponent, BulkConfirmationDialogComponent, BulkStatusDialogComponent, BulkStatusDialogComponent, diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts index 55dc2f8b71..00ec259a12 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/sm-routing.module.ts @@ -5,6 +5,7 @@ import { AuthGuard } from "@bitwarden/angular/auth/guards"; import { organizationEnabledGuard } from "./guards/sm-org-enabled.guard"; import { canActivateSM } from "./guards/sm.guard"; +import { IntegrationsModule } from "./integrations/integrations.module"; import { LayoutComponent } from "./layout/layout.component"; import { NavigationComponent } from "./layout/navigation.component"; import { OverviewModule } from "./overview/overview.module"; @@ -54,12 +55,19 @@ const routes: Routes = [ }, }, { - path: "service-accounts", + path: "machine-accounts", loadChildren: () => ServiceAccountsModule, data: { titleId: "machineAccounts", }, }, + { + path: "integrations", + loadChildren: () => IntegrationsModule, + data: { + titleId: "integrations", + }, + }, { path: "trash", loadChildren: () => TrashModule, diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 5f8c4145cb..445727ac61 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -2,6 +2,8 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -23,6 +25,7 @@ export class CollectionsComponent implements OnInit { collections: CollectionView[] = []; organization: Organization; flexibleCollectionsV1Enabled: boolean; + restrictProviderAccess: boolean; protected cipherDomain: Cipher; @@ -33,9 +36,16 @@ export class CollectionsComponent implements OnInit { protected cipherService: CipherService, protected organizationService: OrganizationService, private logService: LogService, + private configService: ConfigService, ) {} async ngOnInit() { + this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( + FeatureFlag.FlexibleCollectionsV1, + ); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); await this.load(); } @@ -62,7 +72,12 @@ export class CollectionsComponent implements OnInit { async submit(): Promise { const selectedCollectionIds = this.collections .filter((c) => { - if (this.organization.canEditAllCiphers(this.flexibleCollectionsV1Enabled)) { + if ( + this.organization.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ) + ) { return !!(c as any).checked; } else { return !!(c as any).checked && c.readOnly == null; diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 8345bb9939..b5cc50d847 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -23,7 +23,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -93,7 +93,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected apiService: ApiService, protected i18nService: I18nService, protected validationService: ValidationService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected platformUtilsService: PlatformUtilsService, protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, @@ -156,7 +156,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { } private async setRememberDeviceDefaultValue() { - const rememberDeviceFromState = await this.deviceTrustCryptoService.getShouldTrustDevice( + const rememberDeviceFromState = await this.deviceTrustService.getShouldTrustDevice( this.activeAccountId, ); @@ -169,9 +169,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.rememberDevice.valueChanges .pipe( switchMap((value) => - defer(() => - this.deviceTrustCryptoService.setShouldTrustDevice(this.activeAccountId, value), - ), + defer(() => this.deviceTrustService.setShouldTrustDevice(this.activeAccountId, value)), ), takeUntil(this.destroy$), ) @@ -274,6 +272,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { // this.loading to support clients without async-actions-support this.loading = true; + // errors must be caught in child components to prevent navigation try { const { publicKey, privateKey } = await this.cryptoService.initAccount(); const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString); @@ -288,10 +287,8 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { await this.passwordResetEnrollmentService.enroll(this.data.organizationId); if (this.rememberDeviceForm.value.rememberDevice) { - await this.deviceTrustCryptoService.trustDevice(this.activeAccountId); + await this.deviceTrustService.trustDevice(this.activeAccountId); } - } catch (error) { - this.validationService.showError(error); } finally { this.loading = false; } diff --git a/libs/angular/src/auth/components/change-password.component.ts b/libs/angular/src/auth/components/change-password.component.ts index 1086428f4c..b1f75de58c 100644 --- a/libs/angular/src/auth/components/change-password.component.ts +++ b/libs/angular/src/auth/components/change-password.component.ts @@ -3,13 +3,13 @@ import { Subject, takeUntil } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -31,7 +31,6 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { minimumLength = Utils.minimumPasswordLength; protected email: string; - protected kdf: KdfType; protected kdfConfig: KdfConfig; protected destroy$ = new Subject(); @@ -45,6 +44,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { protected policyService: PolicyService, protected stateService: StateService, protected dialogService: DialogService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -73,18 +73,14 @@ export class ChangePasswordComponent implements OnInit, OnDestroy { } const email = await this.stateService.getEmail(); - if (this.kdf == null) { - this.kdf = await this.stateService.getKdfType(); - } if (this.kdfConfig == null) { - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); } // Create new master key const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/components/lock.component.ts b/libs/angular/src/auth/components/lock.component.ts index 6602a917c9..7eb30d759a 100644 --- a/libs/angular/src/auth/components/lock.component.ts +++ b/libs/angular/src/auth/components/lock.component.ts @@ -1,7 +1,7 @@ import { Directive, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom, Subject } from "rxjs"; -import { concatMap, take, takeUntil } from "rxjs/operators"; +import { concatMap, map, take, takeUntil } from "rxjs/operators"; import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -11,9 +11,12 @@ import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abs import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SecretVerificationRequest } from "@bitwarden/common/auth/models/request/secret-verification.request"; import { MasterPasswordPolicyResponse } from "@bitwarden/common/auth/models/response/master-password-policy.response"; @@ -29,6 +32,7 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; import { UserKey } from "@bitwarden/common/types/key"; import { DialogService } from "@bitwarden/components"; @@ -45,6 +49,7 @@ export class LockComponent implements OnInit, OnDestroy { supportsBiometric: boolean; biometricLock: boolean; + private activeUserId: UserId; protected successRoute = "vault"; protected forcePasswordResetRoute = "update-temp-password"; protected onSuccessfulSubmit: () => Promise; @@ -74,18 +79,21 @@ export class LockComponent implements OnInit, OnDestroy { protected policyService: InternalPolicyService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected dialogService: DialogService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected userVerificationService: UserVerificationService, protected pinCryptoService: PinCryptoServiceAbstraction, protected biometricStateService: BiometricStateService, protected accountService: AccountService, + protected authService: AuthService, + protected kdfConfigService: KdfConfigService, ) {} async ngOnInit() { - this.stateService.activeAccount$ + this.accountService.activeAccount$ .pipe( - concatMap(async () => { - await this.load(); + concatMap(async (account) => { + this.activeUserId = account?.id; + await this.load(account?.id); }), takeUntil(this.destroy$), ) @@ -114,7 +122,7 @@ export class LockComponent implements OnInit, OnDestroy { }); if (confirmed) { - this.messagingService.send("logout"); + this.messagingService.send("logout", { userId: this.activeUserId }); } } @@ -208,14 +216,12 @@ export class LockComponent implements OnInit, OnDestroy { } private async doUnlockWithMasterPassword() { + const kdfConfig = await this.kdfConfigService.getKdfConfig(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - const kdf = await this.stateService.getKdfType(); - const kdfConfig = await this.stateService.getKdfConfig(); const masterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email, - kdf, kdfConfig, ); const storedMasterKeyHash = await firstValueFrom( @@ -277,13 +283,12 @@ export class LockComponent implements OnInit, OnDestroy { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); await this.doContinue(evaluatePasswordAfterUnlock); } private async doContinue(evaluatePasswordAfterUnlock: boolean) { - await this.stateService.setEverBeenUnlocked(true); await this.biometricStateService.resetUserPromptCancelled(); this.messagingService.send("unlocked"); @@ -322,23 +327,35 @@ export class LockComponent implements OnInit, OnDestroy { } } - private async load() { + private async load(userId: UserId) { // TODO: Investigate PM-3515 // The loading of the lock component works as follows: - // 1. First, is locking a valid timeout action? If not, we will log the user out. - // 2. If locking IS a valid timeout action, we proceed to show the user the lock screen. + // 1. If the user is unlocked, we're here in error so we navigate to the home page + // 2. First, is locking a valid timeout action? If not, we will log the user out. + // 3. If locking IS a valid timeout action, we proceed to show the user the lock screen. // The user will be able to unlock as follows: // - If they have a PIN set, they will be presented with the PIN input // - If they have a master password and no PIN, they will be presented with the master password input // - If they have biometrics enabled, they will be presented with the biometric prompt + const isUnlocked = await firstValueFrom( + this.authService + .authStatusFor$(userId) + .pipe(map((status) => status === AuthenticationStatus.Unlocked)), + ); + if (isUnlocked) { + // navigate to home + await this.router.navigate(["/"]); + return; + } + const availableVaultTimeoutActions = await firstValueFrom( - this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(), + this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(userId), ); const supportsLock = availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock); if (!supportsLock) { - return await this.vaultTimeoutService.logOut(); + return await this.vaultTimeoutService.logOut(userId); } this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); diff --git a/libs/angular/src/auth/components/login-via-auth-request.component.ts b/libs/angular/src/auth/components/login-via-auth-request.component.ts index 5a1180cd38..a60468e244 100644 --- a/libs/angular/src/auth/components/login-via-auth-request.component.ts +++ b/libs/angular/src/auth/components/login-via-auth-request.component.ts @@ -12,7 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AnonymousHubService } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable"; @@ -86,7 +86,7 @@ export class LoginViaAuthRequestComponent private validationService: ValidationService, private stateService: StateService, private loginEmailService: LoginEmailServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private loginStrategyService: LoginStrategyServiceAbstraction, private accountService: AccountService, @@ -221,7 +221,8 @@ export class LoginViaAuthRequestComponent } // Request still pending response from admin - // So, create hub connection so that any approvals will be received via push notification + // set keypair and create hub connection so that any approvals will be received via push notification + this.authRequestKeyPair = { privateKey: adminAuthReqStorable.privateKey, publicKey: null }; await this.anonymousHubService.createHubConnection(adminAuthReqStorable.id); } @@ -401,7 +402,7 @@ export class LoginViaAuthRequestComponent // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.deviceTrustCryptoService.trustDeviceIfRequired(activeAccount.id); + await this.deviceTrustService.trustDeviceIfRequired(activeAccount.id); // TODO: don't forget to use auto enrollment service everywhere we trust device diff --git a/libs/angular/src/auth/components/register.component.ts b/libs/angular/src/auth/components/register.component.ts index 3cffebe71b..2ba7669290 100644 --- a/libs/angular/src/auth/components/register.component.ts +++ b/libs/angular/src/auth/components/register.component.ts @@ -15,7 +15,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic 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 { DEFAULT_KDF_CONFIG, DEFAULT_KDF_TYPE } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; @@ -273,9 +273,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn name: string, ): Promise { const hint = this.formGroup.value.hint; - const kdf = DEFAULT_KDF_TYPE; const kdfConfig = DEFAULT_KDF_CONFIG; - const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + const key = await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); const newUserKey = await this.cryptoService.makeUserKey(key); const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, key); const keys = await this.cryptoService.makeKeyPair(newUserKey[0]); @@ -287,10 +286,8 @@ export class RegisterComponent extends CaptchaProtectedComponent implements OnIn newUserKey[1].encryptedString, this.referenceData, this.captchaToken, - kdf, + kdfConfig.kdfType, kdfConfig.iterations, - kdfConfig.memory, - kdfConfig.parallelism, ); request.keys = new KeysRequest(keys[0], keys[1].encryptedString); const orgInvite = await this.stateService.getOrganizationInvitation(); diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index eebf87655b..7ddc76d6c1 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -13,6 +13,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { OrganizationAutoEnrollStatusResponse } from "@bitwarden/common/admin-console/models/response/organization-auto-enroll-status.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; @@ -23,11 +24,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - HashPurpose, - DEFAULT_KDF_TYPE, - DEFAULT_KDF_CONFIG, -} from "@bitwarden/common/platform/enums"; +import { HashPurpose, DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @@ -73,6 +70,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -83,6 +81,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -139,7 +138,6 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { } async setupSubmitActions() { - this.kdf = DEFAULT_KDF_TYPE; this.kdfConfig = DEFAULT_KDF_CONFIG; return true; } @@ -169,10 +167,8 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.hint, this.orgSsoIdentifier, keysRequest, - this.kdf, + this.kdfConfig.kdfType, //always PBKDF2 --> see this.setupSubmitActions this.kdfConfig.iterations, - this.kdfConfig.memory, - this.kdfConfig.parallelism, ); try { if (this.resetPasswordAutoEnroll) { @@ -246,11 +242,9 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { ); userDecryptionOpts.hasMasterPassword = true; await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); - - await this.stateService.setKdfType(this.kdf); - await this.stateService.setKdfConfig(this.kdfConfig); + await this.kdfConfigService.setKdfConfig(this.userId, this.kdfConfig); await this.masterPasswordService.setMasterKey(masterKey, this.userId); - await this.cryptoService.setUserKey(userKey[0]); + await this.cryptoService.setUserKey(userKey[0], this.userId); // Set private key only for new JIT provisioned users in MP encryption orgs // Existing TDE users will have private key set on sync or on login @@ -259,7 +253,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { this.forceSetPasswordReason != ForceSetPasswordReason.TdeUserWithoutPasswordHasPasswordResetPermission ) { - await this.cryptoService.setPrivateKey(keyPair[1].encryptedString); + await this.cryptoService.setPrivateKey(keyPair[1].encryptedString, this.userId); } const localMasterKeyHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/components/set-pin.component.ts b/libs/angular/src/auth/components/set-pin.component.ts index ade23f4fef..f0b66b8e70 100644 --- a/libs/angular/src/auth/components/set-pin.component.ts +++ b/libs/angular/src/auth/components/set-pin.component.ts @@ -2,6 +2,7 @@ import { DialogRef } from "@angular/cdk/dialog"; import { Directive, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -22,6 +23,7 @@ export class SetPinComponent implements OnInit { private userVerificationService: UserVerificationService, private stateService: StateService, private formBuilder: FormBuilder, + private kdfConfigService: KdfConfigService, ) {} async ngOnInit() { @@ -43,8 +45,7 @@ export class SetPinComponent implements OnInit { const pinKey = await this.cryptoService.makePinKey( pin, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); const userKey = await this.cryptoService.getUserKey(); const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey); diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 269ec51e30..ae644028f9 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -253,7 +253,7 @@ describe("SsoComponent", () => { describe("2FA scenarios", () => { beforeEach(() => { const authResult = new AuthResult(); - authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); + authResult.twoFactorProviders = { [TwoFactorProviderType.Authenticator]: {} }; // use standard user with MP because this test is not concerned with password reset. selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); diff --git a/libs/angular/src/auth/components/two-factor-options.component.ts b/libs/angular/src/auth/components/two-factor-options.component.ts index 2808e41cc2..1bbf81fa34 100644 --- a/libs/angular/src/auth/components/two-factor-options.component.ts +++ b/libs/angular/src/auth/components/two-factor-options.component.ts @@ -2,7 +2,10 @@ import { Directive, EventEmitter, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; -import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { + TwoFactorProviderDetails, + TwoFactorService, +} from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -24,11 +27,11 @@ export class TwoFactorOptionsComponent implements OnInit { protected environmentService: EnvironmentService, ) {} - ngOnInit() { - this.providers = this.twoFactorService.getSupportedProviders(this.win); + async ngOnInit() { + this.providers = await this.twoFactorService.getSupportedProviders(this.win); } - choose(p: any) { + async choose(p: TwoFactorProviderDetails) { this.onProviderSelected.emit(p.type); } diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index f73f0483be..8e96c48ba0 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -102,7 +102,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI } async ngOnInit() { - if (!(await this.authing()) || this.twoFactorService.getProviders() == null) { + if (!(await this.authing()) || (await this.twoFactorService.getProviders()) == null) { // 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([this.loginRoute]); @@ -145,7 +145,9 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI ); } - this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported); + this.selectedProviderType = await this.twoFactorService.getDefaultProvider( + this.webAuthnSupported, + ); await this.init(); } @@ -162,12 +164,14 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.cleanupWebAuthn(); this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); switch (this.selectedProviderType) { case TwoFactorProviderType.WebAuthn: if (!this.webAuthnNewTab) { - setTimeout(() => { - this.authWebAuthn(); + setTimeout(async () => { + await this.authWebAuthn(); }, 500); } break; @@ -212,7 +216,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI break; case TwoFactorProviderType.Email: this.twoFactorEmail = providerData.Email; - if (this.twoFactorService.getProviders().size > 1) { + if ((await this.twoFactorService.getProviders()).size > 1) { await this.sendEmail(false); } break; @@ -474,8 +478,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.emailPromise = null; } - authWebAuthn() { - const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType); + async authWebAuthn() { + const providerData = await this.twoFactorService.getProviders().then((providers) => { + return providers.get(this.selectedProviderType); + }); if (!this.webAuthnSupported || this.webAuthn == null) { return; diff --git a/libs/angular/src/auth/components/update-password.component.ts b/libs/angular/src/auth/components/update-password.component.ts index 2ffffb6c5d..264f351542 100644 --- a/libs/angular/src/auth/components/update-password.component.ts +++ b/libs/angular/src/auth/components/update-password.component.ts @@ -4,6 +4,7 @@ import { Router } from "@angular/router"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; @@ -44,6 +45,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, private logService: LogService, dialogService: DialogService, + kdfConfigService: KdfConfigService, ) { super( i18nService, @@ -54,6 +56,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -90,8 +93,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent { return false; } - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } diff --git a/libs/angular/src/auth/components/update-temp-password.component.ts b/libs/angular/src/auth/components/update-temp-password.component.ts index 54fdc83239..bd6da6b760 100644 --- a/libs/angular/src/auth/components/update-temp-password.component.ts +++ b/libs/angular/src/auth/components/update-temp-password.component.ts @@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; @@ -59,6 +60,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { private userVerificationService: UserVerificationService, protected router: Router, dialogService: DialogService, + kdfConfigService: KdfConfigService, private accountService: AccountService, private masterPasswordService: InternalMasterPasswordServiceAbstraction, ) { @@ -71,6 +73,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { policyService, stateService, dialogService, + kdfConfigService, ); } @@ -104,8 +107,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { async setupSubmitActions(): Promise { this.email = await this.stateService.getEmail(); - this.kdf = await this.stateService.getKdfType(); - this.kdfConfig = await this.stateService.getKdfConfig(); + this.kdfConfig = await this.kdfConfigService.getKdfConfig(); return true; } @@ -124,7 +126,6 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent { const newMasterKey = await this.cryptoService.makeMasterKey( this.masterPassword, this.email.trim().toLowerCase(), - this.kdf, this.kdfConfig, ); const newPasswordHash = await this.cryptoService.hashMasterKey( diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index 6f71d77a63..8cd5290ebc 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { ClientType } from "@bitwarden/common/enums"; @@ -30,7 +30,7 @@ export function lockGuard(): CanActivateFn { ) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const platformUtilService = inject(PlatformUtilsService); const messagingService = inject(MessagingService); const router = inject(Router); @@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn { // User is authN and in locked state. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); // Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow // The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP diff --git a/libs/angular/src/auth/guards/redirect.guard.ts b/libs/angular/src/auth/guards/redirect.guard.ts index ca9152186d..0c43673c34 100644 --- a/libs/angular/src/auth/guards/redirect.guard.ts +++ b/libs/angular/src/auth/guards/redirect.guard.ts @@ -3,7 +3,7 @@ import { CanActivateFn, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -31,7 +31,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv return async (route) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); @@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the // login decryption options component. - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); diff --git a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts index 146c6e19a2..524ce7dce5 100644 --- a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts +++ b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts @@ -8,7 +8,7 @@ import { import { firstValueFrom } from "rxjs"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -22,11 +22,11 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn { return async (_: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { const authService = inject(AuthService); const cryptoService = inject(CryptoService); - const deviceTrustCryptoService = inject(DeviceTrustCryptoServiceAbstraction); + const deviceTrustService = inject(DeviceTrustServiceAbstraction); const router = inject(Router); const authStatus = await authService.getAuthStatus(); - const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); + const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) { return router.createUrlTree(["/"]); diff --git a/libs/angular/src/components/toastr.component.ts b/libs/angular/src/components/toastr.component.ts deleted file mode 100644 index bfe20ed866..0000000000 --- a/libs/angular/src/components/toastr.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { animate, state, style, transition, trigger } from "@angular/animations"; -import { CommonModule } from "@angular/common"; -import { Component, ModuleWithProviders, NgModule } from "@angular/core"; -import { - DefaultNoComponentGlobalConfig, - GlobalConfig, - Toast as BaseToast, - ToastPackage, - ToastrService, - TOAST_CONFIG, -} from "ngx-toastr"; - -@Component({ - selector: "[toast-component2]", - template: ` - -
- -
-
-
- {{ title }} [{{ duplicatesCount + 1 }}] -
-
-
- {{ message }} -
-
-
-
-
- `, - animations: [ - trigger("flyInOut", [ - state("inactive", style({ opacity: 0 })), - state("active", style({ opacity: 1 })), - state("removed", style({ opacity: 0 })), - transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), - transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), - ]), - ], - preserveWhitespaces: false, -}) -export class BitwardenToast extends BaseToast { - constructor( - protected toastrService: ToastrService, - public toastPackage: ToastPackage, - ) { - super(toastrService, toastPackage); - } -} - -export const BitwardenToastGlobalConfig: GlobalConfig = { - ...DefaultNoComponentGlobalConfig, - toastComponent: BitwardenToast, -}; - -@NgModule({ - imports: [CommonModule], - declarations: [BitwardenToast], - exports: [BitwardenToast], -}) -export class BitwardenToastModule { - static forRoot(config: Partial = {}): ModuleWithProviders { - return { - ngModule: BitwardenToastModule, - providers: [ - { - provide: TOAST_CONFIG, - useValue: { - default: BitwardenToastGlobalConfig, - config: config, - }, - }, - ], - }; - } -} diff --git a/libs/angular/src/directives/if-feature.directive.spec.ts b/libs/angular/src/directives/if-feature.directive.spec.ts index 944410be7d..456220b791 100644 --- a/libs/angular/src/directives/if-feature.directive.spec.ts +++ b/libs/angular/src/directives/if-feature.directive.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock, MockProxy } from "jest-mock-extended"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -41,10 +41,8 @@ describe("IfFeatureDirective", () => { let content: HTMLElement; let mockConfigService: MockProxy; - const mockConfigFlagValue = (flag: FeatureFlag, flagValue: FeatureFlagValue) => { - mockConfigService.getFeatureFlag.mockImplementation((f, defaultValue) => - flag == f ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), - ); + const mockConfigFlagValue = (flag: FeatureFlag, flagValue: AllowedFeatureFlagTypes) => { + mockConfigService.getFeatureFlag.mockImplementation((f) => Promise.resolve(flagValue as any)); }; const queryContent = (testId: string) => diff --git a/libs/angular/src/directives/if-feature.directive.ts b/libs/angular/src/directives/if-feature.directive.ts index 069f306a89..838bd264ad 100644 --- a/libs/angular/src/directives/if-feature.directive.ts +++ b/libs/angular/src/directives/if-feature.directive.ts @@ -1,6 +1,6 @@ import { Directive, Input, OnInit, TemplateRef, ViewContainerRef } from "@angular/core"; -import { FeatureFlag, FeatureFlagValue } from "@bitwarden/common/enums/feature-flag.enum"; +import { AllowedFeatureFlagTypes, FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -23,7 +23,7 @@ export class IfFeatureDirective implements OnInit { * Optional value to compare against the value of the feature flag in the config service. * @default true */ - @Input() appIfFeatureValue: FeatureFlagValue = true; + @Input() appIfFeatureValue: AllowedFeatureFlagTypes = true; private hasView = false; diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 64fb44e3b8..5f1bf796aa 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -2,10 +2,9 @@ import { CommonModule, DatePipe } from "@angular/common"; import { NgModule } from "@angular/core"; import { FormsModule, ReactiveFormsModule } from "@angular/forms"; -import { AutofocusDirective } from "@bitwarden/components"; +import { AutofocusDirective, ToastModule } from "@bitwarden/components"; import { CalloutComponent } from "./components/callout.component"; -import { BitwardenToastModule } from "./components/toastr.component"; import { A11yInvalidDirective } from "./directives/a11y-invalid.directive"; import { A11yTitleDirective } from "./directives/a11y-title.directive"; import { ApiActionDirective } from "./directives/api-action.directive"; @@ -34,7 +33,7 @@ import { IconComponent } from "./vault/components/icon.component"; @NgModule({ imports: [ - BitwardenToastModule.forRoot({ + ToastModule.forRoot({ maxOpened: 5, autoDismiss: true, closeButton: true, @@ -77,7 +76,7 @@ import { IconComponent } from "./vault/components/icon.component"; A11yTitleDirective, ApiActionDirective, AutofocusDirective, - BitwardenToastModule, + ToastModule, BoxRowDirective, CalloutComponent, CopyTextDirective, diff --git a/libs/angular/src/pipes/user-name.pipe.ts b/libs/angular/src/pipes/user-name.pipe.ts index 88b088a7e2..f007f4ad87 100644 --- a/libs/angular/src/pipes/user-name.pipe.ts +++ b/libs/angular/src/pipes/user-name.pipe.ts @@ -1,6 +1,6 @@ import { Pipe, PipeTransform } from "@angular/core"; -interface User { +export interface User { name?: string; email?: string; } diff --git a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts index 88637dff97..323e8c2659 100644 --- a/libs/angular/src/platform/guard/feature-flag.guard.spec.ts +++ b/libs/angular/src/platform/guard/feature-flag.guard.spec.ts @@ -34,12 +34,12 @@ describe("canAccessFeature", () => { flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), ); } else if (typeof flagValue === "string") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = "") => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(""), ); } else if (typeof flagValue === "number") { - mockConfigService.getFeatureFlag.mockImplementation((flag, defaultValue = 0) => - flag == testFlag ? Promise.resolve(flagValue) : Promise.resolve(defaultValue), + mockConfigService.getFeatureFlag.mockImplementation((flag) => + flag == testFlag ? Promise.resolve(flagValue as any) : Promise.resolve(0), ); } diff --git a/libs/angular/src/platform/services/broadcaster.service.ts b/libs/angular/src/platform/services/broadcaster.service.ts deleted file mode 100644 index cf58d2b311..0000000000 --- a/libs/angular/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from "@angular/core"; - -import { BroadcasterService as BaseBroadcasterService } from "@bitwarden/common/platform/services/broadcaster.service"; - -@Injectable() -export class BroadcasterService extends BaseBroadcasterService {} diff --git a/libs/angular/src/platform/services/logging-error-handler.ts b/libs/angular/src/platform/services/logging-error-handler.ts index 522412dd28..5644272d35 100644 --- a/libs/angular/src/platform/services/logging-error-handler.ts +++ b/libs/angular/src/platform/services/logging-error-handler.ts @@ -14,7 +14,7 @@ export class LoggingErrorHandler extends ErrorHandler { override handleError(error: any): void { try { const logService = this.injector.get(LogService, null); - logService.error(error); + logService.error("Unhandled error in angular", error); } catch { super.handleError(error); } diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg index e815389126..bc0a348fee 100644 --- a/libs/angular/src/scss/bwicons/fonts/bwi-font.svg +++ b/libs/angular/src/scss/bwicons/fonts/bwi-font.svg @@ -5,11 +5,11 @@ - + @@ -154,7 +154,7 @@ - + @@ -187,6 +187,17 @@ + + + + + + + + + + + diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf index f9b63283e0..f70eea7af7 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf and b/libs/angular/src/scss/bwicons/fonts/bwi-font.ttf differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff index 1e57b1aab3..52cecc3ead 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff differ diff --git a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 index 88036b7b3e..4c8cfd6e04 100644 Binary files a/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 and b/libs/angular/src/scss/bwicons/fonts/bwi-font.woff2 differ diff --git a/libs/angular/src/scss/bwicons/styles/style.scss b/libs/angular/src/scss/bwicons/styles/style.scss index af13e0ddb6..e1333da468 100644 --- a/libs/angular/src/scss/bwicons/styles/style.scss +++ b/libs/angular/src/scss/bwicons/styles/style.scss @@ -104,10 +104,13 @@ $icons: ( "universal-access": "\e991", "save-changes": "\e988", "browser": "\e985", + "browser-alt": "\e9a3", "mobile": "\e986", + "mobile-alt": "\e9a4", "cli": "\e987", "providers": "\e983", "vault": "\e984", + "vault-f": "\e9ab", "folder-closed-f": "\e982", "rocket": "\e9ee", "ellipsis-h": "\e9ef", @@ -131,9 +134,11 @@ $icons: ( "hamburger": "\e972", "bw-folder-open-f1": "\e93e", "desktop": "\e96a", + "desktop-alt": "\e9a2", "angle-up": "\e969", "user": "\e900", "user-f": "\e901", + "user-monitor": "\e9a7", "key": "\e902", "share-square": "\e903", "hashtag": "\e904", @@ -157,6 +162,7 @@ $icons: ( "files": "\e916", "trash": "\e917", "plus": "\e918", + "plus-f": "\e9a9", "star": "\e919", "list": "\e91a", "angle-down": "\e92d", @@ -237,6 +243,7 @@ $icons: ( "linkedin": "\e955", "discourse": "\e91e", "twitter": "\e961", + "x-twitter": "\e9a5", "youtube": "\e966", "windows": "\e964", "apple": "\e945", @@ -265,6 +272,10 @@ $icons: ( "caret-down": "\e99e", "passkey": "\e99f", "lock-encrypted": "\e9a0", + "back": "\e9a8", + "popout": "\e9aa", + "wand": "\e9a6", + "msp": "\e9a1", ); @each $name, $glyph in $icons { diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 7d39078797..c58931ce55 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -1,13 +1,14 @@ import { InjectionToken } from "@angular/core"; -import { Observable } from "rxjs"; +import { Observable, Subject } from "rxjs"; +import { ClientType } from "@bitwarden/common/enums"; import { - AbstractMemoryStorageService, AbstractStorageService, ObservableStorageService, } from "@bitwarden/common/platform/abstractions/storage.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message } from "@bitwarden/common/platform/messaging"; declare const tag: unique symbol; /** @@ -22,7 +23,7 @@ export class SafeInjectionToken extends InjectionToken { export const WINDOW = new SafeInjectionToken("WINDOW"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< - AbstractMemoryStorageService & ObservableStorageService + AbstractStorageService & ObservableStorageService >("OBSERVABLE_MEMORY_STORAGE"); export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService @@ -30,12 +31,9 @@ export const OBSERVABLE_DISK_STORAGE = new SafeInjectionToken< export const OBSERVABLE_DISK_LOCAL_STORAGE = new SafeInjectionToken< AbstractStorageService & ObservableStorageService >("OBSERVABLE_DISK_LOCAL_STORAGE"); -export const MEMORY_STORAGE = new SafeInjectionToken( - "MEMORY_STORAGE", -); +export const MEMORY_STORAGE = new SafeInjectionToken("MEMORY_STORAGE"); export const SECURE_STORAGE = new SafeInjectionToken("SECURE_STORAGE"); export const STATE_FACTORY = new SafeInjectionToken("STATE_FACTORY"); -export const STATE_SERVICE_USE_CACHE = new SafeInjectionToken("STATE_SERVICE_USE_CACHE"); export const LOGOUT_CALLBACK = new SafeInjectionToken< (expired: boolean, userId?: string) => Promise >("LOGOUT_CALLBACK"); @@ -49,3 +47,7 @@ export const LOG_MAC_FAILURES = new SafeInjectionToken("LOG_MAC_FAILURE export const SYSTEM_THEME_OBSERVABLE = new SafeInjectionToken>( "SYSTEM_THEME_OBSERVABLE", ); +export const INTRAPROCESS_MESSAGING_SUBJECT = new SafeInjectionToken>>( + "INTRAPROCESS_MESSAGING_SUBJECT", +); +export const CLIENT_TYPE = new SafeInjectionToken("CLIENT_TYPE"); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index ad0881a4b3..fdbf5e9ecb 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -1,4 +1,5 @@ import { ErrorHandler, LOCALE_ID, NgModule } from "@angular/core"; +import { Subject } from "rxjs"; import { AuthRequestServiceAbstraction, @@ -58,9 +59,10 @@ import { import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/auth/abstractions/anonymous-hub.service"; import { AuthService as AuthServiceAbstraction } from "@bitwarden/common/auth/abstractions/auth.service"; import { AvatarService as AvatarServiceAbstraction } from "@bitwarden/common/auth/abstractions/avatar.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; import { DevicesServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices/devices.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction, @@ -80,9 +82,10 @@ import { AccountServiceImplementation } from "@bitwarden/common/auth/services/ac import { AnonymousHubService } from "@bitwarden/common/auth/services/anonymous-hub.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AvatarService } from "@bitwarden/common/auth/services/avatar.service"; -import { DeviceTrustCryptoService } from "@bitwarden/common/auth/services/device-trust-crypto.service.implementation"; +import { DeviceTrustService } from "@bitwarden/common/auth/services/device-trust.service.implementation"; import { DevicesServiceImplementation } from "@bitwarden/common/auth/services/devices/devices.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/auth/services/devices-api.service.implementation"; +import { KdfConfigService } from "@bitwarden/common/auth/services/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/services/key-connector.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { PasswordResetEnrollmentServiceImplementation } from "@bitwarden/common/auth/services/password-reset-enrollment.service.implementation"; @@ -115,7 +118,7 @@ import { BillingApiService } from "@bitwarden/common/billing/services/billing-ap import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service"; import { PaymentMethodWarningsService } from "@bitwarden/common/billing/services/payment-method-warnings.service"; import { AppIdService as AppIdServiceAbstraction } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { BroadcasterService as BroadcasterServiceAbstraction } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -136,6 +139,9 @@ import { DefaultBiometricStateService, } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateFactory } from "@bitwarden/common/platform/factories/state-factory"; +import { Message, MessageListener, MessageSender } from "@bitwarden/common/platform/messaging"; +// eslint-disable-next-line no-restricted-imports -- Used for dependency injection +import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { devFlagEnabled, flagEnabled } from "@bitwarden/common/platform/misc/flags"; import { Account } from "@bitwarden/common/platform/models/domain/account"; import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; @@ -146,6 +152,7 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { CryptoService } from "@bitwarden/common/platform/services/crypto.service"; import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation"; import { MultithreadEncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/multithread-encrypt.service.implementation"; +import { DefaultBroadcasterService } from "@bitwarden/common/platform/services/default-broadcaster.service"; import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/default-environment.service"; import { FileUploadService } from "@bitwarden/common/platform/services/file-upload/file-upload.service"; import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; @@ -154,6 +161,7 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r import { NoopNotificationsService } from "@bitwarden/common/platform/services/noop-notifications.service"; import { StateService } from "@bitwarden/common/platform/services/state.service"; import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider"; +import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service"; import { ValidationService } from "@bitwarden/common/platform/services/validation.service"; import { WebCryptoFunctionService } from "@bitwarden/common/platform/services/web-crypto-function.service"; import { @@ -245,7 +253,6 @@ import { import { AuthGuard } from "../auth/guards/auth.guard"; import { UnauthGuard } from "../auth/guards/unauth.guard"; import { FormValidationErrorsService as FormValidationErrorsServiceAbstraction } from "../platform/abstractions/form-validation-errors.service"; -import { BroadcasterService } from "../platform/services/broadcaster.service"; import { FormValidationErrorsService } from "../platform/services/form-validation-errors.service"; import { LoggingErrorHandler } from "../platform/services/logging-error-handler"; import { AngularThemingService } from "../platform/services/theming/angular-theming.service"; @@ -263,11 +270,12 @@ import { SafeInjectionToken, SECURE_STORAGE, STATE_FACTORY, - STATE_SERVICE_USE_CACHE, SUPPORTS_SECURE_STORAGE, SYSTEM_LANGUAGE, SYSTEM_THEME_OBSERVABLE, WINDOW, + INTRAPROCESS_MESSAGING_SUBJECT, + CLIENT_TYPE, } from "./injection-tokens"; import { ModalService } from "./modal.service"; @@ -306,10 +314,6 @@ const safeProviders: SafeProvider[] = [ provide: STATE_FACTORY, useValue: new StateFactory(GlobalState, Account), }), - safeProvider({ - provide: STATE_SERVICE_USE_CACHE, - useValue: true, - }), safeProvider({ provide: LOGOUT_CALLBACK, useFactory: @@ -383,11 +387,12 @@ const safeProviders: SafeProvider[] = [ EncryptService, PasswordStrengthServiceAbstraction, PolicyServiceAbstraction, - DeviceTrustCryptoServiceAbstraction, + DeviceTrustServiceAbstraction, AuthRequestServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -541,6 +546,7 @@ const safeProviders: SafeProvider[] = [ StateServiceAbstraction, AccountServiceAbstraction, StateProvider, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -621,9 +627,14 @@ const safeProviders: SafeProvider[] = [ AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, + TokenServiceAbstraction, ], }), - safeProvider({ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService, deps: [] }), + safeProvider({ + provide: BroadcasterService, + useClass: DefaultBroadcasterService, + deps: [MessageSender, MessageListener], + }), safeProvider({ provide: VaultTimeoutSettingsServiceAbstraction, useClass: VaultTimeoutSettingsService, @@ -645,7 +656,6 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, FolderServiceAbstraction, CollectionServiceAbstraction, - CryptoServiceAbstraction, PlatformUtilsServiceAbstraction, MessagingServiceAbstraction, SearchServiceAbstraction, @@ -679,7 +689,6 @@ const safeProviders: SafeProvider[] = [ EnvironmentService, TokenServiceAbstraction, MigrationRunner, - STATE_SERVICE_USE_CACHE, ], }), safeProvider({ @@ -707,7 +716,7 @@ const safeProviders: SafeProvider[] = [ CipherServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -718,8 +727,8 @@ const safeProviders: SafeProvider[] = [ ApiServiceAbstraction, CryptoServiceAbstraction, CryptoFunctionServiceAbstraction, - StateServiceAbstraction, CollectionServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -828,6 +837,7 @@ const safeProviders: SafeProvider[] = [ LogService, VaultTimeoutSettingsServiceAbstraction, PlatformUtilsServiceAbstraction, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -863,7 +873,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: TwoFactorServiceAbstraction, useClass: TwoFactorService, - deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction], + deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction, GlobalStateProvider], }), safeProvider({ provide: FormValidationErrorsServiceAbstraction, @@ -943,8 +953,8 @@ const safeProviders: SafeProvider[] = [ deps: [DevicesApiServiceAbstraction], }), safeProvider({ - provide: DeviceTrustCryptoServiceAbstraction, - useClass: DeviceTrustCryptoService, + provide: DeviceTrustServiceAbstraction, + useClass: DeviceTrustService, deps: [ KeyGenerationServiceAbstraction, CryptoFunctionServiceAbstraction, @@ -957,6 +967,7 @@ const safeProviders: SafeProvider[] = [ StateProvider, SECURE_STORAGE, UserDecryptionOptionsServiceAbstraction, + LogService, ], }), safeProvider({ @@ -979,6 +990,7 @@ const safeProviders: SafeProvider[] = [ CryptoServiceAbstraction, VaultTimeoutSettingsServiceAbstraction, LogService, + KdfConfigServiceAbstraction, ], }), safeProvider({ @@ -1035,7 +1047,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: DerivedStateProvider, useClass: DefaultDerivedStateProvider, - deps: [OBSERVABLE_MEMORY_STORAGE], + deps: [], }), safeProvider({ provide: StateProvider, @@ -1051,10 +1063,12 @@ const safeProviders: SafeProvider[] = [ provide: OrganizationBillingServiceAbstraction, useClass: OrganizationBillingService, deps: [ + ApiServiceAbstraction, CryptoServiceAbstraction, EncryptService, I18nServiceAbstraction, OrganizationApiServiceAbstraction, + SyncServiceAbstraction, ], }), safeProvider({ @@ -1085,7 +1099,7 @@ const safeProviders: SafeProvider[] = [ safeProvider({ provide: MigrationRunner, useClass: MigrationRunner, - deps: [AbstractStorageService, LogService, MigrationBuilderService], + deps: [AbstractStorageService, LogService, MigrationBuilderService, CLIENT_TYPE], }), safeProvider({ provide: MigrationBuilderService, @@ -1112,16 +1126,41 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultOrganizationManagementPreferencesService, deps: [StateProvider], }), + safeProvider({ + provide: UserAutoUnlockKeyService, + useClass: UserAutoUnlockKeyService, + deps: [CryptoServiceAbstraction], + }), safeProvider({ provide: ErrorHandler, useClass: LoggingErrorHandler, deps: [], }), + safeProvider({ + provide: INTRAPROCESS_MESSAGING_SUBJECT, + useFactory: () => new Subject>(), + deps: [], + }), + safeProvider({ + provide: MessageListener, + useFactory: (subject: Subject>) => new MessageListener(subject.asObservable()), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), + safeProvider({ + provide: MessageSender, + useFactory: (subject: Subject>) => new SubjectMessageSender(subject), + deps: [INTRAPROCESS_MESSAGING_SUBJECT], + }), safeProvider({ provide: ProviderApiServiceAbstraction, useClass: ProviderApiService, deps: [ApiServiceAbstraction], }), + safeProvider({ + provide: KdfConfigServiceAbstraction, + useClass: KdfConfigService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( diff --git a/libs/angular/src/tools/send/add-edit.component.ts b/libs/angular/src/tools/send/add-edit.component.ts index da859e50bf..b4f7ec171a 100644 --- a/libs/angular/src/tools/send/add-edit.component.ts +++ b/libs/angular/src/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { Subject, firstValueFrom, takeUntil, map, BehaviorSubject, concatMap } f import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; +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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -118,6 +119,7 @@ export class AddEditComponent implements OnInit, OnDestroy { protected dialogService: DialogService, protected formBuilder: FormBuilder, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected accountService: AccountService, ) { this.typeOptions = [ { name: i18nService.t("sendTypeFile"), value: SendType.File, premium: true }, @@ -215,7 +217,9 @@ export class AddEditComponent implements OnInit, OnDestroy { } async load() { - this.emailVerified = await this.stateService.getEmailVerified(); + this.emailVerified = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.emailVerified ?? false)), + ); this.type = !this.canAccessPremium || !this.emailVerified ? SendType.Text : SendType.File; if (this.send == null) { diff --git a/libs/angular/src/utils/component-route-swap.ts b/libs/angular/src/utils/component-route-swap.ts new file mode 100644 index 0000000000..1a2db317d6 --- /dev/null +++ b/libs/angular/src/utils/component-route-swap.ts @@ -0,0 +1,55 @@ +import { Type } from "@angular/core"; +import { Route, Routes } from "@angular/router"; + +/** + * Helper function to swap between two components based on an async condition. The async condition is evaluated + * as an `CanMatchFn` and supports Angular dependency injection via `inject()`. + * + * @example + * ```ts + * const routes = [ + * ...componentRouteSwap( + * defaultComponent, + * altComponent, + * async () => { + * const configService = inject(ConfigService); + * return configService.getFeatureFlag(FeatureFlag.SomeFlag); + * }, + * { + * path: 'some-path' + * } + * ), + * // Other routes... + * ]; + * ``` + * + * @param defaultComponent - The default component to render. + * @param altComponent - The alternate component to render when the condition is met. + * @param shouldSwapFn - The async function to determine if the alternate component should be rendered. + * @param options - The shared route options to apply to both components. + */ +export function componentRouteSwap( + defaultComponent: Type, + altComponent: Type, + shouldSwapFn: () => Promise, + options: Route, +): Routes { + const defaultRoute = { + ...options, + component: defaultComponent, + }; + + const altRoute: Route = { + ...options, + component: altComponent, + canMatch: [ + async () => { + return await shouldSwapFn(); + }, + ...(options.canMatch ?? []), + ], + }; + + // Return the alternate route first, so it is evaluated first. + return [altRoute, defaultRoute]; +} diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index d29c74b42d..74c368d726 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -91,6 +91,7 @@ export class AddEditComponent implements OnInit, OnDestroy { private previousCipherId: string; protected flexibleCollectionsV1Enabled = false; + protected restrictProviderAccess = false; get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -182,10 +183,10 @@ export class AddEditComponent implements OnInit, OnDestroy { async ngOnInit() { this.flexibleCollectionsV1Enabled = await this.configService.getFeatureFlag( FeatureFlag.FlexibleCollectionsV1, - false, ); - this.writeableCollections = await this.loadCollections(); - this.canUseReprompt = await this.passwordRepromptService.enabled(); + this.restrictProviderAccess = await this.configService.getFeatureFlag( + FeatureFlag.RestrictProviderAccess, + ); this.policyService .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) @@ -197,6 +198,9 @@ export class AddEditComponent implements OnInit, OnDestroy { takeUntil(this.destroy$), ) .subscribe(); + + this.writeableCollections = await this.loadCollections(); + this.canUseReprompt = await this.passwordRepromptService.enabled(); } ngOnDestroy() { @@ -289,6 +293,16 @@ export class AddEditComponent implements OnInit, OnDestroy { }); } } + // Only Admins can clone a cipher to different owner + if (this.cloneMode && this.cipher.organizationId != null) { + const cipherOrg = (await firstValueFrom(this.organizationService.memberOrganizations$)).find( + (o) => o.id === this.cipher.organizationId, + ); + + if (cipherOrg != null && !cipherOrg.isAdmin && !cipherOrg.permissions.editAnyCollection) { + this.ownershipOptions = [{ name: cipherOrg.name, value: cipherOrg.id }]; + } + } // We don't want to copy passkeys when we clone a cipher if (this.cloneMode && this.cipher?.login?.hasFido2Credentials) { @@ -658,11 +672,14 @@ export class AddEditComponent implements OnInit, OnDestroy { protected saveCipher(cipher: Cipher) { const isNotClone = this.editMode && !this.cloneMode; - let orgAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + let orgAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); // if a cipher is unassigned we want to check if they are an admin or have permission to edit any collection if (!cipher.collectionIds) { - orgAdmin = this.organization?.canEditUnassignedCiphers(); + orgAdmin = this.organization?.canEditUnassignedCiphers(this.restrictProviderAccess); } return this.cipher.id == null @@ -671,14 +688,20 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected deleteCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipher.isDeleted ? this.cipherService.deleteWithServer(this.cipher.id, asAdmin) : this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); } protected restoreCipher() { - const asAdmin = this.organization?.canEditAllCiphers(this.flexibleCollectionsV1Enabled); + const asAdmin = this.organization?.canEditAllCiphers( + this.flexibleCollectionsV1Enabled, + this.restrictProviderAccess, + ); return this.cipherService.restoreWithServer(this.cipher.id, asAdmin); } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index 42349737f0..27d6e14b11 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -9,7 +9,7 @@ import { OnInit, Output, } from "@angular/core"; -import { firstValueFrom, Subject, takeUntil } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -69,7 +69,6 @@ export class ViewComponent implements OnDestroy, OnInit { private totpInterval: any; private previousCipherId: string; private passwordReprompted = false; - private directiveIsDestroyed$ = new Subject(); get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); @@ -119,19 +118,11 @@ export class ViewComponent implements OnDestroy, OnInit { } }); }); - - this.billingAccountProfileStateService.hasPremiumFromAnySource$ - .pipe(takeUntil(this.directiveIsDestroyed$)) - .subscribe((canAccessPremium: boolean) => { - this.canAccessPremium = canAccessPremium; - }); } ngOnDestroy() { this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); this.cleanUp(); - this.directiveIsDestroyed$.next(true); - this.directiveIsDestroyed$.complete(); } async load() { @@ -141,6 +132,9 @@ export class ViewComponent implements OnDestroy, OnInit { this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher), ); + this.canAccessPremium = await firstValueFrom( + this.billingAccountProfileStateService.hasPremiumFromAnySource$, + ); this.showPremiumRequiredTotp = this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp; diff --git a/libs/auth/src/angular/anon-layout/anon-layout.component.html b/libs/auth/src/angular/anon-layout/anon-layout.component.html index 1f583edf20..55da36d9bf 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.html +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.html @@ -1,5 +1,5 @@
@@ -13,8 +13,10 @@

{{ subtitle }}

-
-
+
+
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 d247a010bf..106844fb5a 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.component.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.component.ts @@ -5,7 +5,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { IconModule, Icon } from "../../../../components/src/icon"; import { TypographyModule } from "../../../../components/src/typography"; -import { BitwardenLogo } from "../../icons/bitwarden-logo"; +import { BitwardenLogo } from "../icons/bitwarden-logo.icon"; @Component({ standalone: true, diff --git a/libs/auth/src/angular/anon-layout/anon-layout.mdx b/libs/auth/src/angular/anon-layout/anon-layout.mdx new file mode 100644 index 0000000000..c604c02f03 --- /dev/null +++ b/libs/auth/src/angular/anon-layout/anon-layout.mdx @@ -0,0 +1,118 @@ +import { Meta, Story, Controls } from "@storybook/addon-docs"; + +import * as stories from "./anon-layout.stories"; + + + +# AnonLayout Component + +The Auth-owned AnonLayoutComponent is to be used for unauthenticated pages, where we don't know who +the user is (this includes viewing a Send). + +--- + +### Incorrect Usage ❌ + +The AnonLayoutComponent is **not** to be implemented by every component that uses it in that +component's template directly. For example, if you have a component template called +`example.component.html`, and you want it to use the AnonLayoutComponent, you will **not** be +writing: + +```html + + + +
Example component content
+
+``` + +### Correct Usage ✅ + +Instead the AnonLayoutComponent is implemented solely in the router via routable composition, which +gives us the advantages of nested routes in Angular. + +To allow for routable composition, Auth will also provide a wrapper component in each client, called +AnonLayout**Wrapper**Component. + +For clarity: + +- AnonLayoutComponent = the Auth-owned library component - `` +- AnonLayout**Wrapper**Component = the client-specific wrapper component to be used in a client + routing module + +The AnonLayout**Wrapper**Component embeds the AnonLayoutComponent along with the router outlets: + +```html + + + + + + +``` + +To implement, the developer does not need to work with the base AnonLayoutComponent directly. The +devoloper simply uses the AnonLayout**Wrapper**Component in `oss-routing.module.ts` (for Web, for +example) to construct the page via routable composition: + +```javascript +// File: oss-routing.module.ts + +{ + path: "", + component: AnonLayoutWrapperComponent, // Wrapper component + children: [ + { + path: "sample-route", // replace with your route + children: [ + { + path: "", + component: MyPrimaryComponent, // replace with your component + }, + { + path: "", + component: MySecondaryComponent, // replace with your component (or remove this secondary outlet object entirely if not needed) + outlet: "secondary", + }, + ], + data: { + pageTitle: "logIn", // example of a translation key from messages.json + pageSubtitle: "loginWithMasterPassword", // example of a translation key from messages.json + pageIcon: LockIcon, // example of an icon to pass in + }, + }, + ], + }, +``` + +And if the AnonLayout**Wrapper**Component is already being used in your client's routing module, +then your work will be as simple as just adding another child route under the `children` array. + +### Data Properties + +In the `oss-routing.module.ts` example above, notice the data properties being passed in: + +- For the `pageTitle` and `pageSubtitle` - pass in a translation key from `messages.json`. +- For the `pageIcon` - import an icon (of type `Icon`) into the router file and use the icon + directly. + +All 3 of these properties are optional. + +```javascript +import { LockIcon } from "@bitwarden/auth/angular"; + +// ... + +{ + // ... + data: { + pageTitle: "logIn", + pageSubtitle: "loginWithMasterPassword", + pageIcon: LockIcon, + }, +} +``` + +--- + + 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 daba5b5e53..61a395b155 100644 --- a/libs/auth/src/angular/anon-layout/anon-layout.stories.ts +++ b/libs/auth/src/angular/anon-layout/anon-layout.stories.ts @@ -3,12 +3,12 @@ import { Meta, StoryObj, moduleMetadata } from "@storybook/angular"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ButtonModule } from "../../../../components/src/button"; -import { IconLock } from "../../icons/icon-lock"; +import { LockIcon } from "../icons"; import { AnonLayoutComponent } from "./anon-layout.component"; class MockPlatformUtilsService implements Partial { - getApplicationVersion = () => Promise.resolve("Version 2023.1.1"); + getApplicationVersion = () => Promise.resolve("Version 2024.1.1"); } export default { @@ -28,7 +28,7 @@ export default { args: { title: "The Page Title", subtitle: "The subtitle (optional)", - icon: IconLock, + icon: LockIcon, }, } as Meta; @@ -38,14 +38,13 @@ export const WithPrimaryContent: Story = { render: (args) => ({ props: args, template: - /** - * The projected content (i.e. the
) and styling below is just a - * sample and could be replaced with any content and styling - */ + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. ` -
Primary Projected Content Area (customizable)
-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
+
Primary Projected Content Area (customizable)
+
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
+
`, }), @@ -55,15 +54,16 @@ export const WithSecondaryContent: Story = { render: (args) => ({ props: args, template: - // Notice that slot="secondary" is requred to project any secondary content: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + // Notice that slot="secondary" is requred to project any secondary content. ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
-
+
Secondary Projected Content (optional)
@@ -75,14 +75,16 @@ export const WithSecondaryContent: Story = { export const WithLongContent: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
's) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam? Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
-
+
Secondary Projected Content (optional)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Lorem ipsum dolor sit amet consectetur adipisicing elit. Molestias laborum nostrum natus. Expedita, quod est?

@@ -95,9 +97,11 @@ export const WithLongContent: Story = { export const WithIcon: Story = { render: (args) => ({ props: args, - template: ` + template: + // Projected content (the
) and styling is just a sample and can be replaced with any content/styling. + ` -
+
Primary Projected Content Area (customizable)
Lorem ipsum dolor sit amet consectetur adipisicing elit. Necessitatibus illum vero, placeat recusandae esse ratione eius minima veniam nemo, quas beatae! Impedit molestiae alias sapiente explicabo. Sapiente corporis ipsa numquam?
diff --git a/libs/auth/src/icons/bitwarden-logo.ts b/libs/auth/src/angular/icons/bitwarden-logo.icon.ts similarity index 100% rename from libs/auth/src/icons/bitwarden-logo.ts rename to libs/auth/src/angular/icons/bitwarden-logo.icon.ts diff --git a/libs/auth/src/angular/icons/index.ts b/libs/auth/src/angular/icons/index.ts index 7bb3f57579..d71e2e6efd 100644 --- a/libs/auth/src/angular/icons/index.ts +++ b/libs/auth/src/angular/icons/index.ts @@ -1 +1,3 @@ +export * from "./bitwarden-logo.icon"; +export * from "./lock.icon"; export * from "./user-verification-biometrics-fingerprint.icon"; diff --git a/libs/auth/src/icons/icon-lock.ts b/libs/auth/src/angular/icons/lock.icon.ts similarity index 98% rename from libs/auth/src/icons/icon-lock.ts rename to libs/auth/src/angular/icons/lock.icon.ts index 61330fe0df..b567c213f7 100644 --- a/libs/auth/src/icons/icon-lock.ts +++ b/libs/auth/src/angular/icons/lock.icon.ts @@ -1,6 +1,6 @@ import { svgIcon } from "@bitwarden/components"; -export const IconLock = svgIcon` +export const LockIcon = svgIcon` diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index c93bf1c1d3..067ed63b8e 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -5,6 +5,7 @@ // icons export * from "./icons"; +export * from "./anon-layout/anon-layout.component"; export * from "./fingerprint-dialog/fingerprint-dialog.component"; export * from "./password-callout/password-callout.component"; diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 0ce6c9fed7..cde64f0477 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -1,7 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response"; @@ -42,8 +43,9 @@ describe("AuthRequestLoginStrategy", () => { let stateService: MockProxy; let twoFactorService: MockProxy; let userDecryptionOptions: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -75,15 +77,18 @@ describe("AuthRequestLoginStrategy", () => { stateService = mock(); twoFactorService = mock(); userDecryptionOptions = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); accountService = mockAccountServiceWith(mockUserId); masterPasswordService = new FakeMasterPasswordService(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: mockUserId, + }); authRequestLoginStrategy = new AuthRequestLoginStrategy( cache, @@ -99,8 +104,9 @@ describe("AuthRequestLoginStrategy", () => { stateService, twoFactorService, userDecryptionOptions, - deviceTrustCryptoService, + deviceTrustService, billingAccountProfileStateService, + kdfConfigService, ); tokenResponse = identityTokenResponseFactory(); @@ -122,6 +128,7 @@ describe("AuthRequestLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); await authRequestLoginStrategy.logIn(credentials); @@ -132,8 +139,8 @@ describe("AuthRequestLoginStrategy", () => { ); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); }); it("sets keys after a successful authentication when only userKey provided in login credentials", async () => { @@ -157,9 +164,9 @@ describe("AuthRequestLoginStrategy", () => { // setMasterKeyEncryptedUserKey, setUserKey, and setPrivateKey should still be called expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); expect(cryptoService.setUserKey).toHaveBeenCalledWith(decUserKey); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, mockUserId); // trustDeviceIfRequired should be called - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); }); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 4035a7be58..e815d8f3ba 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -3,7 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -18,6 +18,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -61,8 +62,9 @@ export class AuthRequestLoginStrategy extends LoginStrategy { stateService: StateService, twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -78,6 +80,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -114,13 +117,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy { return super.logInTwoFactor(twoFactor); } - protected override async setMasterKey(response: IdentityTokenResponse) { + protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { const authRequestCredentials = this.cache.value.authRequestCredentials; if ( authRequestCredentials.decryptedMasterKey && authRequestCredentials.decryptedMasterKeyHash ) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setMasterKey( authRequestCredentials.decryptedMasterKey, userId, @@ -144,15 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy { if (authRequestCredentials.decryptedUserKey) { await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey); } else { - await this.trySetUserKeyWithMasterKey(); + await this.trySetUserKeyWithMasterKey(userId); // Establish trust if required after setting user key - await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); + await this.deviceTrustService.trustDeviceIfRequired(userId); } } - private async trySetUserKeyWithMasterKey(): Promise { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + private async trySetUserKeyWithMasterKey(userId: UserId): Promise { const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); @@ -160,9 +161,13 @@ export class AuthRequestLoginStrategy extends LoginStrategy { } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 431f736e94..612222c10e 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -24,12 +25,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { - Account, - AccountProfile, - AccountTokens, - AccountKeys, -} from "@bitwarden/common/platform/models/domain/account"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; @@ -118,6 +114,7 @@ describe("LoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -137,6 +134,7 @@ describe("LoginStrategy", () => { stateService = mock(); twoFactorService = mock(); userDecryptionOptionsService = mock(); + kdfConfigService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -163,6 +161,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); }); @@ -209,14 +208,8 @@ describe("LoginStrategy", () => { userId: userId, name: name, email: email, - kdfIterations: kdfIterations, - kdfType: kdf, }, }, - tokens: { - ...new AccountTokens(), - }, - keys: new AccountKeys(), }), ); expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( @@ -225,6 +218,22 @@ describe("LoginStrategy", () => { expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); + it("throws if new account isn't active after being initialized", async () => { + const idTokenResponse = identityTokenResponseFactory(); + apiService.postIdentityToken.mockResolvedValue(idTokenResponse); + + const mockVaultTimeoutAction = VaultTimeoutAction.Lock; + const mockVaultTimeout = 1000; + + stateService.getVaultTimeoutAction.mockResolvedValue(mockVaultTimeoutAction); + stateService.getVaultTimeout.mockResolvedValue(mockVaultTimeout); + + accountService.switchAccount = jest.fn(); // block internal switch to new account + accountService.activeAccountSubject.next(null); // simulate no active account + + await expect(async () => await passwordLoginStrategy.logIn(credentials)).rejects.toThrow(); + }); + it("builds AuthResult", async () => { const tokenResponse = identityTokenResponseFactory(); tokenResponse.forcePasswordReset = true; @@ -235,6 +244,7 @@ describe("LoginStrategy", () => { const result = await passwordLoginStrategy.logIn(credentials); expect(result).toEqual({ + userId: userId, forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset, resetMasterPassword: true, twoFactorProviders: null, @@ -308,8 +318,10 @@ describe("LoginStrategy", () => { expect(tokenService.clearTwoFactorToken).toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(0, null); + expected.twoFactorProviders = { 0: null } as Record< + TwoFactorProviderType, + Record + >; expect(result).toEqual(expected); }); @@ -338,8 +350,9 @@ describe("LoginStrategy", () => { expect(messagingService.send).not.toHaveBeenCalled(); const expected = new AuthResult(); - expected.twoFactorProviders = new Map(); - expected.twoFactorProviders.set(1, { Email: "k***@bitwarden.com" }); + expected.twoFactorProviders = { + [TwoFactorProviderType.Email]: { Email: "k***@bitwarden.com" }, + }; expected.email = userEmail; expected.ssoEmail2FaSessionToken = ssoEmail2FaSessionToken; @@ -408,6 +421,7 @@ describe("LoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory()); diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index a6dc193183..adcf753325 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -1,13 +1,15 @@ -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, filter, firstValueFrom, timeout } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request"; import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; @@ -27,11 +29,8 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { - Account, - AccountProfile, - AccountTokens, -} from "@bitwarden/common/platform/models/domain/account"; +import { KdfType } from "@bitwarden/common/platform/enums"; +import { Account, AccountProfile } from "@bitwarden/common/platform/models/domain/account"; import { UserId } from "@bitwarden/common/types/guid"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -76,6 +75,7 @@ export abstract class LoginStrategy { protected twoFactorService: TwoFactorService, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected KdfConfigService: KdfConfigService, ) {} abstract exportCache(): CacheData; @@ -101,7 +101,7 @@ export abstract class LoginStrategy { } protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> { - this.twoFactorService.clearSelectedProvider(); + await this.twoFactorService.clearSelectedProvider(); const tokenRequest = this.cache.value.tokenRequest; const response = await this.apiService.postIdentityToken(tokenRequest); @@ -159,16 +159,22 @@ export abstract class LoginStrategy { * It also sets the access token and refresh token in the token service. * * @param {IdentityTokenResponse} tokenResponse - The response from the server containing the identity token. - * @returns {Promise} - A promise that resolves when the account information has been successfully saved. + * @returns {Promise} - A promise that resolves the the UserId when the account information has been successfully saved. */ protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const accountInformation = await this.tokenService.decodeAccessToken(tokenResponse.accessToken); - const userId = accountInformation.sub; + const userId = accountInformation.sub as UserId; const vaultTimeoutAction = await this.stateService.getVaultTimeoutAction({ userId }); const vaultTimeout = await this.stateService.getVaultTimeout({ userId }); + await this.accountService.addAccount(userId, { + name: accountInformation.name, + email: accountInformation.email, + emailVerified: accountInformation.email_verified, + }); + // set access token and refresh token before account initialization so authN status can be accurate // User id will be derived from the access token. await this.tokenService.setTokens( @@ -178,6 +184,8 @@ export abstract class LoginStrategy { tokenResponse.refreshToken, // Note: CLI login via API key sends undefined for refresh token. ); + await this.accountService.switchAccount(userId); + await this.stateService.addAccount( new Account({ profile: { @@ -186,24 +194,30 @@ export abstract class LoginStrategy { userId, name: accountInformation.name, email: accountInformation.email, - kdfIterations: tokenResponse.kdfIterations, - kdfMemory: tokenResponse.kdfMemory, - kdfParallelism: tokenResponse.kdfParallelism, - kdfType: tokenResponse.kdf, }, }, - tokens: { - ...new AccountTokens(), - }, }), ); + await this.verifyAccountAdded(userId); + await this.userDecryptionOptionsService.setUserDecryptionOptions( UserDecryptionOptions.fromResponse(tokenResponse), ); + await this.KdfConfigService.setKdfConfig( + userId as UserId, + tokenResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(tokenResponse.kdfIterations) + : new Argon2KdfConfig( + tokenResponse.kdfIterations, + tokenResponse.kdfMemory, + tokenResponse.kdfParallelism, + ), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); - return userId as UserId; + return userId; } protected async processTokenResponse(response: IdentityTokenResponse): Promise { @@ -227,6 +241,7 @@ export abstract class LoginStrategy { // Must come before setting keys, user key needs email to update additional keys const userId = await this.saveAccountInformation(response); + result.userId = userId; if (response.twoFactorToken != null) { // note: we can read email from access token b/c it was saved in saveAccountInformation @@ -235,9 +250,9 @@ export abstract class LoginStrategy { await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken); } - await this.setMasterKey(response); + await this.setMasterKey(response, userId); await this.setUserKey(response, userId); - await this.setPrivateKey(response); + await this.setPrivateKey(response, userId); this.messagingService.send("loggedIn"); @@ -245,9 +260,9 @@ export abstract class LoginStrategy { } // The keys comes from different sources depending on the login strategy - protected abstract setMasterKey(response: IdentityTokenResponse): Promise; + protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise; protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise; - protected abstract setPrivateKey(response: IdentityTokenResponse): Promise; + protected abstract setPrivateKey(response: IdentityTokenResponse, userId: UserId): Promise; // Old accounts used master key for encryption. We are forcing migrations but only need to // check on password logins @@ -255,9 +270,10 @@ export abstract class LoginStrategy { return false; } - protected async createKeyPairForOldAccount() { + protected async createKeyPairForOldAccount(userId: UserId) { try { - const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(); + const userKey = await this.cryptoService.getUserKeyWithLegacySupport(userId); + const [publicKey, privateKey] = await this.cryptoService.makeKeyPair(userKey); await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString)); return privateKey.encryptedString; } catch (e) { @@ -280,7 +296,7 @@ export abstract class LoginStrategy { const result = new AuthResult(); result.twoFactorProviders = response.twoFactorProviders2; - this.twoFactorService.setProviders(response); + await this.twoFactorService.setProviders(response); this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null }); result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken; result.email = response.email; @@ -302,4 +318,24 @@ export abstract class LoginStrategy { result.captchaSiteKey = response.siteKey; return result; } + + /** + * Verifies that the active account is set after initialization. + * Note: In browser there is a slight delay between when active account emits in background, + * and when it emits in foreground. We're giving the foreground 1 second to catch up. + * If nothing is emitted, we throw an error. + */ + private async verifyAccountAdded(expectedUserId: UserId) { + await firstValueFrom( + this.accountService.activeAccount$.pipe( + filter((account) => account?.id === expectedUserId), + timeout({ + first: 1000, + with: () => { + throw new Error("Expected user never made active user after initialization."); + }, + }), + ), + ); + } } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index b902fff574..f887f047a9 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; @@ -71,6 +72,7 @@ describe("PasswordLoginStrategy", () => { let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let passwordLoginStrategy: PasswordLoginStrategy; let credentials: PasswordLoginCredentials; @@ -94,9 +96,12 @@ describe("PasswordLoginStrategy", () => { policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); loginStrategyService.makePreloginKey.mockResolvedValue(masterKey); @@ -127,6 +132,7 @@ describe("PasswordLoginStrategy", () => { policyService, loginStrategyService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new PasswordLoginCredentials(email, masterPassword); tokenResponse = identityTokenResponseFactory(masterPasswordPolicy); @@ -158,6 +164,7 @@ describe("PasswordLoginStrategy", () => { masterPasswordService.masterKeySubject.next(masterKey); cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); await passwordLoginStrategy.logIn(credentials); @@ -166,9 +173,12 @@ describe("PasswordLoginStrategy", () => { localHashedPassword, userId, ); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + tokenResponse.key, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("does not force the user to update their master password when there are no requirements", async () => { @@ -193,6 +203,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const result = await passwordLoginStrategy.logIn(credentials); @@ -207,6 +218,7 @@ describe("PasswordLoginStrategy", () => { it("forces the user to update their master password on successful 2FA login when it does not meet master password policy requirements", async () => { passwordStrengthService.getPasswordStrength.mockReturnValue({ score: 0 } as any); policyService.evaluateMasterPassword.mockReturnValue(false); + tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); const token2FAResponse = new IdentityTwoFactorResponse({ TwoFactorProviders: ["0"], diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 2490c35a00..80048d6e10 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -89,6 +90,7 @@ export class PasswordLoginStrategy extends LoginStrategy { private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -104,6 +106,7 @@ export class PasswordLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -144,6 +147,10 @@ export class PasswordLoginStrategy extends LoginStrategy { const [authResult, identityResponse] = await this.startLogIn(); + if (identityResponse instanceof IdentityCaptchaResponse) { + return authResult; + } + const masterPasswordPolicyOptions = this.getMasterPasswordPolicyOptionsFromResponse(identityResponse); @@ -154,23 +161,23 @@ export class PasswordLoginStrategy extends LoginStrategy { credentials, masterPasswordPolicyOptions, ); + if (meetsRequirements) { + return authResult; + } - if (!meetsRequirements) { - if (authResult.requiresCaptcha || authResult.requiresTwoFactor) { - // Save the flag to this strategy for later use as the master password is about to pass out of scope - this.cache.next({ - ...this.cache.value, - forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword, - }); - } else { - // Authentication was successful, save the force update password options with the state service - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason( - ForceSetPasswordReason.WeakMasterPassword, - userId, - ); - authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; - } + if (identityResponse instanceof IdentityTwoFactorResponse) { + // Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope + this.cache.next({ + ...this.cache.value, + forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword, + }); + } else { + // Authentication was successful, save the force update password options with the state service + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + authResult.userId, // userId is only available on successful login + ); + authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword; } } return authResult; @@ -193,17 +200,18 @@ export class PasswordLoginStrategy extends LoginStrategy { !result.requiresCaptcha && forcePasswordResetReason != ForceSetPasswordReason.None ) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; - await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId); + await this.masterPasswordService.setForceSetPasswordReason( + forcePasswordResetReason, + result.userId, + ); result.forcePasswordReset = forcePasswordResetReason; } return result; } - protected override async setMasterKey(response: IdentityTokenResponse) { + protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { const { masterKey, localMasterKeyHash } = this.cache.value; - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId); } @@ -216,18 +224,22 @@ export class PasswordLoginStrategy extends LoginStrategy { if (this.encryptionKeyMigrationRequired(response)) { return; } - await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); + await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey); + await this.cryptoService.setUserKey(userKey, userId); } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } @@ -236,9 +248,9 @@ export class PasswordLoginStrategy extends LoginStrategy { } private getMasterPasswordPolicyOptionsFromResponse( - response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse, + response: IdentityTokenResponse | IdentityTwoFactorResponse, ): MasterPasswordPolicyOptions { - if (response == null || response instanceof IdentityCaptchaResponse) { + if (response == null) { return null; } return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index b78ad6dea6..1e4d867603 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -1,7 +1,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -50,10 +51,11 @@ describe("SsoLoginStrategy", () => { let twoFactorService: MockProxy; let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let authRequestService: MockProxy; let i18nService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let ssoLoginStrategy: SsoLoginStrategy; let credentials: SsoLoginCredentials; @@ -82,14 +84,17 @@ describe("SsoLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); keyConnectorService = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); authRequestService = mock(); i18nService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); ssoLoginStrategy = new SsoLoginStrategy( null, @@ -106,10 +111,11 @@ describe("SsoLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, keyConnectorService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, i18nService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new SsoLoginCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId); }); @@ -157,7 +163,10 @@ describe("SsoLoginStrategy", () => { // Assert expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + tokenResponse.key, + userId, + ); }); describe("Trusted Device Decryption", () => { @@ -209,8 +218,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); const cryptoSvcSetUserKeySpy = jest.spyOn(cryptoService, "setUserKey"); @@ -218,8 +227,8 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); // Assert - expect(deviceTrustCryptoService.getDeviceKey).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.getDeviceKey).toHaveBeenCalledTimes(1); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledTimes(1); expect(cryptoSvcSetUserKeySpy).toHaveBeenCalledWith(mockUserKey); }); @@ -232,8 +241,8 @@ describe("SsoLoginStrategy", () => { ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); // Set deviceKey to be null - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(null); - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); + deviceTrustService.getDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(mockUserKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -254,7 +263,7 @@ describe("SsoLoginStrategy", () => { // Arrange const idTokenResponse = mockIdTokenResponseWithModifiedTrustedDeviceOption(valueName, null); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Act await ssoLoginStrategy.logIn(credentials); @@ -271,9 +280,9 @@ describe("SsoLoginStrategy", () => { userDecryptionOptsServerResponseWithTdeOption, ); apiService.postIdentityToken.mockResolvedValue(idTokenResponse); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue(mockDeviceKey); + deviceTrustService.getDeviceKey.mockResolvedValue(mockDeviceKey); // Set userKey to be null - deviceTrustCryptoService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); + deviceTrustService.decryptUserKeyWithDeviceKey.mockResolvedValue(null); // Act await ssoLoginStrategy.logIn(credentials); @@ -321,7 +330,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("sets the user key from approved admin request if exists", async () => { @@ -338,7 +347,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled(); }); it("attempts to establish a trusted device if successful", async () => { @@ -355,7 +364,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).toHaveBeenCalled(); }); it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => { @@ -369,7 +378,7 @@ describe("SsoLoginStrategy", () => { authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash, ).not.toHaveBeenCalled(); expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled(); - expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled(); + expect(deviceTrustService.trustDeviceIfRequired).not.toHaveBeenCalled(); }); it("attempts to login with a trusted device if admin auth request isn't successful", async () => { @@ -382,11 +391,11 @@ describe("SsoLoginStrategy", () => { }; apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse); cryptoService.hasUserKey.mockResolvedValue(false); - deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); + deviceTrustService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any); await ssoLoginStrategy.logIn(credentials); - expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); + expect(deviceTrustService.decryptUserKeyWithDeviceKey).toHaveBeenCalled(); }); }); }); @@ -411,7 +420,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl); + expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId); }); it("converts new SSO user with no master password to Key Connector on first login", async () => { @@ -424,6 +433,7 @@ describe("SsoLoginStrategy", () => { expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith( tokenResponse, ssoOrgId, + userId, ); }); @@ -462,7 +472,7 @@ describe("SsoLoginStrategy", () => { await ssoLoginStrategy.logIn(credentials); - expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl); + expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId); }); it("converts new SSO user with no master password to Key Connector on first login", async () => { @@ -475,6 +485,7 @@ describe("SsoLoginStrategy", () => { expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith( tokenResponse, ssoOrgId, + userId, ); }); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index d8efd78984..c37ef683ed 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -3,11 +3,12 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; @@ -22,6 +23,7 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/src/auth/abstractions/device-trust.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; import { @@ -94,10 +96,11 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private keyConnectorService: KeyConnectorService, - private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + private deviceTrustService: DeviceTrustServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, private i18nService: I18nService, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -113,6 +116,7 @@ export class SsoLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -121,7 +125,7 @@ export class SsoLoginStrategy extends LoginStrategy { this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken)); } - async logIn(credentials: SsoLoginCredentials) { + async logIn(credentials: SsoLoginCredentials): Promise { const data = new SsoLoginStrategyData(); data.orgId = credentials.orgId; @@ -144,10 +148,9 @@ export class SsoLoginStrategy extends LoginStrategy { // Auth guard currently handles redirects for this. if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setForceSetPasswordReason( ssoAuthResult.forcePasswordReset, - userId, + ssoAuthResult.userId, ); } @@ -160,7 +163,7 @@ export class SsoLoginStrategy extends LoginStrategy { return ssoAuthResult; } - protected override async setMasterKey(tokenResponse: IdentityTokenResponse) { + protected override async setMasterKey(tokenResponse: IdentityTokenResponse, userId: UserId) { // The only way we can be setting a master key at this point is if we are using Key Connector. // First, check to make sure that we should do so based on the token response. if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) { @@ -172,10 +175,11 @@ export class SsoLoginStrategy extends LoginStrategy { await this.keyConnectorService.convertNewSsoUserToKeyConnector( tokenResponse, this.cache.value.orgId, + userId, ); } else { const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse); - await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl); + await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId); } } } @@ -228,7 +232,7 @@ export class SsoLoginStrategy extends LoginStrategy { if (masterKeyEncryptedUserKey) { // set the master key encrypted user key if it exists - await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey); + await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); } const userDecryptionOptions = tokenResponse?.userDecryptionOptions; @@ -237,18 +241,18 @@ export class SsoLoginStrategy extends LoginStrategy { if (userDecryptionOptions?.trustedDeviceOption) { await this.trySetUserKeyWithApprovedAdminRequestIfExists(userId); - const hasUserKey = await this.cryptoService.hasUserKey(); + const hasUserKey = await this.cryptoService.hasUserKey(userId); // Only try to set user key with device key if admin approval request was not successful if (!hasUserKey) { - await this.trySetUserKeyWithDeviceKey(tokenResponse); + await this.trySetUserKeyWithDeviceKey(tokenResponse, userId); } } else if ( masterKeyEncryptedUserKey != null && this.getKeyConnectorUrl(tokenResponse) != null ) { // Key connector enabled for user - await this.trySetUserKeyWithMasterKey(); + await this.trySetUserKeyWithMasterKey(userId); } // Note: In the traditional SSO flow with MP without key connector, the lock component @@ -298,7 +302,7 @@ export class SsoLoginStrategy extends LoginStrategy { if (await this.cryptoService.hasUserKey()) { // Now that we have a decrypted user key in memory, we can check if we // need to establish trust on the current device - await this.deviceTrustCryptoService.trustDeviceIfRequired(userId); + await this.deviceTrustService.trustDeviceIfRequired(userId); // if we successfully decrypted the user key, we can delete the admin auth request out of state // TODO: eventually we post and clean up DB as well once consumed on client @@ -309,12 +313,13 @@ export class SsoLoginStrategy extends LoginStrategy { } } - private async trySetUserKeyWithDeviceKey(tokenResponse: IdentityTokenResponse): Promise { + private async trySetUserKeyWithDeviceKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { const trustedDeviceOption = tokenResponse.userDecryptionOptions?.trustedDeviceOption; - const userId = (await this.stateService.getUserId()) as UserId; - - const deviceKey = await this.deviceTrustCryptoService.getDeviceKey(userId); + const deviceKey = await this.deviceTrustService.getDeviceKey(userId); const encDevicePrivateKey = trustedDeviceOption?.encryptedPrivateKey; const encUserKey = trustedDeviceOption?.encryptedUserKey; @@ -322,7 +327,7 @@ export class SsoLoginStrategy extends LoginStrategy { return; } - const userKey = await this.deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const userKey = await this.deviceTrustService.decryptUserKeyWithDeviceKey( userId, encDevicePrivateKey, encUserKey, @@ -334,8 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy { } } - private async trySetUserKeyWithMasterKey(): Promise { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + private async trySetUserKeyWithMasterKey(userId: UserId): Promise { const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); // There is a scenario in which the master key is not set here. That will occur if the user @@ -350,12 +354,16 @@ export class SsoLoginStrategy extends LoginStrategy { await this.cryptoService.setUserKey(userKey); } - protected override async setPrivateKey(tokenResponse: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + tokenResponse: IdentityTokenResponse, + userId: UserId, + ): Promise { const newSsoUser = tokenResponse.key == null; if (!newSsoUser) { await this.cryptoService.setPrivateKey( - tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount()), + tokenResponse.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } } diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 5e7d7985b1..673fadd5b0 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -2,6 +2,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -49,6 +50,7 @@ describe("UserApiLoginStrategy", () => { let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let apiLogInStrategy: UserApiLoginStrategy; let credentials: UserApiLoginCredentials; @@ -76,10 +78,13 @@ describe("UserApiLoginStrategy", () => { keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); appIdService.getAppId.mockResolvedValue(deviceId); tokenService.getTwoFactorToken.mockResolvedValue(null); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); apiLogInStrategy = new UserApiLoginStrategy( cache, @@ -98,6 +103,7 @@ describe("UserApiLoginStrategy", () => { environmentService, keyConnectorService, billingAccountProfileStateService, + kdfConfigService, ); credentials = new UserApiLoginCredentials(apiClientId, apiClientSecret); @@ -153,7 +159,7 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey, userId); }); it("gets and sets the master key if Key Connector is enabled", async () => { @@ -168,7 +174,7 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); - expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl); + expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId); }); it("decrypts and sets the user key if Key Connector is enabled", async () => { @@ -189,6 +195,6 @@ describe("UserApiLoginStrategy", () => { await apiLogInStrategy.logIn(credentials); expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); }); }); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 4a0d005b1c..440dccd12e 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; @@ -57,6 +58,7 @@ export class UserApiLoginStrategy extends LoginStrategy { private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class UserApiLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); } @@ -90,11 +93,11 @@ export class UserApiLoginStrategy extends LoginStrategy { return authResult; } - protected override async setMasterKey(response: IdentityTokenResponse) { + protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { if (response.apiUseKeyConnector) { const env = await firstValueFrom(this.environmentService.environment$); const keyConnectorUrl = env.getKeyConnectorUrl(); - await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl); + await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId); } } @@ -105,21 +108,25 @@ export class UserApiLoginStrategy extends LoginStrategy { await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); if (response.apiUseKeyConnector) { - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); if (masterKey) { const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); - await this.cryptoService.setUserKey(userKey); + await this.cryptoService.setUserKey(userKey, userId); } } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } + // Overridden to save client ID and secret to token service protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise { const userId = await super.saveAccountInformation(tokenResponse); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 1d96921286..afac2c2e6a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -1,6 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; @@ -17,7 +18,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { FakeAccountService } from "@bitwarden/common/spec"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; @@ -42,11 +44,13 @@ describe("WebAuthnLoginStrategy", () => { let twoFactorService!: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; const token = "mockToken"; const deviceId = Utils.newGuid(); + const userId = Utils.newGuid() as UserId; let webAuthnCredentials!: WebAuthnLoginCredentials; @@ -67,7 +71,7 @@ describe("WebAuthnLoginStrategy", () => { beforeEach(() => { jest.clearAllMocks(); - accountService = new FakeAccountService(null); + accountService = mockAccountServiceWith(userId); masterPasswordService = new FakeMasterPasswordService(); cryptoService = mock(); @@ -81,10 +85,13 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); + kdfConfigService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); appIdService.getAppId.mockResolvedValue(deviceId); - tokenService.decodeAccessToken.mockResolvedValue({}); + tokenService.decodeAccessToken.mockResolvedValue({ + sub: userId, + }); webAuthnLoginStrategy = new WebAuthnLoginStrategy( cache, @@ -101,6 +108,7 @@ describe("WebAuthnLoginStrategy", () => { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); // Create credentials @@ -200,7 +208,10 @@ describe("WebAuthnLoginStrategy", () => { // Assert // Master key encrypted user key should be set expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1); - expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(idTokenResponse.key); + expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith( + idTokenResponse.key, + userId, + ); expect(cryptoService.decryptToBytes).toHaveBeenCalledTimes(1); expect(cryptoService.decryptToBytes).toHaveBeenCalledWith( @@ -212,8 +223,8 @@ describe("WebAuthnLoginStrategy", () => { idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString, mockPrfPrivateKey, ); - expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey); - expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId); + expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey, userId); // Master key and private key should not be set expect(masterPasswordService.mock.setMasterKey).not.toHaveBeenCalled(); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index 8a62a8fb3c..4b5441d00a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -3,6 +3,7 @@ import { Jsonify } from "type-fest"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -57,6 +58,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService: TwoFactorService, userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, + kdfConfigService: KdfConfigService, ) { super( accountService, @@ -72,6 +74,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { twoFactorService, userDecryptionOptionsService, billingAccountProfileStateService, + kdfConfigService, ); this.cache = new BehaviorSubject(data); @@ -95,7 +98,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { throw new Error("2FA not supported yet for WebAuthn Login."); } - protected override async setMasterKey() { + protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) { return Promise.resolve(); } @@ -104,7 +107,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { if (masterKeyEncryptedUserKey) { // set the master key encrypted user key if it exists - await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey); + await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId); } const userDecryptionOptions = idTokenResponse?.userDecryptionOptions; @@ -131,14 +134,18 @@ export class WebAuthnLoginStrategy extends LoginStrategy { ); if (userKey) { - await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey); + await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId); } } } - protected override async setPrivateKey(response: IdentityTokenResponse): Promise { + protected override async setPrivateKey( + response: IdentityTokenResponse, + userId: UserId, + ): Promise { await this.cryptoService.setPrivateKey( - response.privateKey ?? (await this.createKeyPairForOldAccount()), + response.privateKey ?? (await this.createKeyPairForOldAccount(userId)), + userId, ); } diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index fcc0220d0a..f1b5590404 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -2,7 +2,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; @@ -62,10 +63,11 @@ describe("LoginStrategyService", () => { let encryptService: MockProxy; let passwordStrengthService: MockProxy; let policyService: MockProxy; - let deviceTrustCryptoService: MockProxy; + let deviceTrustService: MockProxy; let authRequestService: MockProxy; let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; + let kdfConfigService: MockProxy; let stateProvider: FakeGlobalStateProvider; let loginStrategyCacheExpirationState: FakeGlobalState; @@ -90,11 +92,12 @@ describe("LoginStrategyService", () => { encryptService = mock(); passwordStrengthService = mock(); policyService = mock(); - deviceTrustCryptoService = mock(); + deviceTrustService = mock(); authRequestService = mock(); userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); + kdfConfigService = mock(); sut = new LoginStrategyService( accountService, @@ -114,11 +117,12 @@ describe("LoginStrategyService", () => { encryptService, passwordStrengthService, policyService, - deviceTrustCryptoService, + deviceTrustService, authRequestService, userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, + kdfConfigService, ); loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY); 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 a8bd7bc2ff..13cca69b3a 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 @@ -10,14 +10,18 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request"; import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response"; @@ -33,9 +37,10 @@ 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 { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; +import { KdfType } from "@bitwarden/common/platform/enums/kdf-type.enum"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state"; +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"; @@ -100,11 +105,12 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected encryptService: EncryptService, protected passwordStrengthService: PasswordStrengthServiceAbstraction, protected policyService: PolicyService, - protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, + protected deviceTrustService: DeviceTrustServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction, protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, + protected kdfConfigService: KdfConfigService, ) { this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY); this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY); @@ -233,24 +239,25 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - let kdf: KdfType = null; let kdfConfig: KdfConfig = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - kdf = preloginResponse.kdf; - kdfConfig = new KdfConfig( - preloginResponse.kdfIterations, - preloginResponse.kdfMemory, - preloginResponse.kdfParallelism, - ); + kdfConfig = + preloginResponse.kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(preloginResponse.kdfIterations) + : new Argon2KdfConfig( + preloginResponse.kdfIterations, + preloginResponse.kdfMemory, + preloginResponse.kdfParallelism, + ); } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig); + return await this.cryptoService.makeMasterKey(masterPassword, email, kdfConfig); } // TODO: move to auth request service @@ -354,6 +361,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.policyService, this, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.Sso: return new SsoLoginStrategy( @@ -371,10 +379,11 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.keyConnectorService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.authRequestService, this.i18nService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.UserApiKey: return new UserApiLoginStrategy( @@ -394,6 +403,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.AuthRequest: return new AuthRequestLoginStrategy( @@ -410,8 +420,9 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.stateService, this.twoFactorService, this.userDecryptionOptionsService, - this.deviceTrustCryptoService, + this.deviceTrustService, this.billingAccountProfileStateService, + this.kdfConfigService, ); case AuthenticationType.WebAuthn: return new WebAuthnLoginStrategy( @@ -429,6 +440,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.twoFactorService, this.userDecryptionOptionsService, this.billingAccountProfileStateService, + this.kdfConfigService, ); } }), diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts index 149d5d9a53..85d36b8d73 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.implementation.ts @@ -1,9 +1,9 @@ import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { KdfType } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { UserKey } from "@bitwarden/common/types/key"; @@ -16,6 +16,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { private cryptoService: CryptoService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private logService: LogService, + private kdfConfigService: KdfConfigService, ) {} async decryptUserKeyWithPin(pin: string): Promise { try { @@ -24,8 +25,7 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } = await this.getPinKeyEncryptedKeys(pinLockType); - const kdf: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); let userKey: UserKey; const email = await this.stateService.getEmail(); if (oldPinKeyEncryptedMasterKey) { @@ -33,7 +33,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { pinLockType === "TRANSIENT", pin, email, - kdf, kdfConfig, oldPinKeyEncryptedMasterKey, ); @@ -41,7 +40,6 @@ export class PinCryptoService implements PinCryptoServiceAbstraction { userKey = await this.cryptoService.decryptUserKeyWithPin( pin, email, - kdf, kdfConfig, pinKeyEncryptedUserKey, ); diff --git a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts index 17e0e14c51..c6fddf8efb 100644 --- a/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts +++ b/libs/auth/src/common/services/pin-crypto/pin-crypto.service.spec.ts @@ -1,9 +1,10 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { @@ -13,6 +14,7 @@ import { import { UserKey } from "@bitwarden/common/types/key"; import { PinCryptoService } from "./pin-crypto.service.implementation"; + describe("PinCryptoService", () => { let pinCryptoService: PinCryptoService; @@ -20,6 +22,7 @@ describe("PinCryptoService", () => { const cryptoService = mock(); const vaultTimeoutSettingsService = mock(); const logService = mock(); + const kdfConfigService = mock(); beforeEach(() => { jest.clearAllMocks(); @@ -29,6 +32,7 @@ describe("PinCryptoService", () => { cryptoService, vaultTimeoutSettingsService, logService, + kdfConfigService, ); }); @@ -39,7 +43,6 @@ describe("PinCryptoService", () => { describe("decryptUserKeyWithPin(...)", () => { const mockPin = "1234"; const mockProtectedPin = "protectedPin"; - const DEFAULT_PBKDF2_ITERATIONS = 600000; const mockUserEmail = "user@example.com"; const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey; @@ -49,7 +52,7 @@ describe("PinCryptoService", () => { ) { vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(DEFAULT_PBKDF2_ITERATIONS)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); stateService.getEmail.mockResolvedValue(mockUserEmail); if (migrationStatus === "PRE") { diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts index 16479f19ea..ae1813d3d7 100644 --- a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -65,6 +65,7 @@ describe("UserDecryptionOptionsService", () => { await fakeAccountService.addAccount(givenUser, { name: "Test User 1", email: "test1@email.com", + emailVerified: false, }); await fakeStateProvider.setUserState( USER_DECRYPTION_OPTIONS, diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts index a8b09b7417..649a158d75 100644 --- a/libs/common/spec/fake-account-service.ts +++ b/libs/common/spec/fake-account-service.ts @@ -1,5 +1,5 @@ import { mock } from "jest-mock-extended"; -import { ReplaySubject } from "rxjs"; +import { ReplaySubject, combineLatest, map } from "rxjs"; import { AccountInfo, AccountService } from "../src/auth/abstractions/account.service"; import { UserId } from "../src/types/guid"; @@ -7,15 +7,20 @@ import { UserId } from "../src/types/guid"; export function mockAccountServiceWith( userId: UserId, info: Partial = {}, + activity: Record = {}, ): FakeAccountService { const fullInfo: AccountInfo = { ...info, ...{ name: "name", email: "email", + emailVerified: true, }, }; - const service = new FakeAccountService({ [userId]: fullInfo }); + + const fullActivity = { [userId]: new Date(), ...activity }; + + const service = new FakeAccountService({ [userId]: fullInfo }, fullActivity); service.activeAccountSubject.next({ id: userId, ...fullInfo }); return service; } @@ -26,17 +31,46 @@ export class FakeAccountService implements AccountService { accountsSubject = new ReplaySubject>(1); // eslint-disable-next-line rxjs/no-exposed-subjects -- test class activeAccountSubject = new ReplaySubject<{ id: UserId } & AccountInfo>(1); + // eslint-disable-next-line rxjs/no-exposed-subjects -- test class + accountActivitySubject = new ReplaySubject>(1); private _activeUserId: UserId; get activeUserId() { return this._activeUserId; } accounts$ = this.accountsSubject.asObservable(); activeAccount$ = this.activeAccountSubject.asObservable(); + accountActivity$ = this.accountActivitySubject.asObservable(); + get sortedUserIds$() { + return this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => a.lastActive.getTime() - b.lastActive.getTime()) + .map((a) => a.userId); + }), + ); + } + get nextUpAccount$() { + return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); + } - constructor(initialData: Record) { + constructor(initialData: Record, accountActivity?: Record) { this.accountsSubject.next(initialData); this.activeAccountSubject.subscribe((data) => (this._activeUserId = data?.id)); this.activeAccountSubject.next(null); + this.accountActivitySubject.next(accountActivity); + } + setAccountActivity(userId: UserId, lastActivity: Date): Promise { + this.accountActivitySubject.next({ + ...this.accountActivitySubject["_buffer"][0], + [userId]: lastActivity, + }); + return this.mock.setAccountActivity(userId, lastActivity); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { @@ -53,10 +87,27 @@ export class FakeAccountService implements AccountService { await this.mock.setAccountEmail(userId, email); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise { + await this.mock.setAccountEmailVerified(userId, emailVerified); + } + async switchAccount(userId: UserId): Promise { const next = userId == null ? null : { id: userId, ...this.accountsSubject["_buffer"]?.[0]?.[userId] }; this.activeAccountSubject.next(next); await this.mock.switchAccount(userId); } + + async clean(userId: UserId): Promise { + const current = this.accountsSubject["_buffer"][0] ?? {}; + const updated = { ...current, [userId]: loggedOutInfo }; + this.accountsSubject.next(updated); + await this.mock.clean(userId); + } } + +const loggedOutInfo: AccountInfo = { + name: undefined, + email: "", + emailVerified: false, +}; diff --git a/libs/common/spec/index.ts b/libs/common/spec/index.ts index 72bd28aca4..90ee121896 100644 --- a/libs/common/spec/index.ts +++ b/libs/common/spec/index.ts @@ -4,3 +4,5 @@ export * from "./matchers"; export * from "./fake-state-provider"; export * from "./fake-state"; export * from "./fake-account-service"; +export * from "./fake-storage.service"; +export * from "./observable-tracker"; diff --git a/libs/common/spec/intercept-console.ts b/libs/common/spec/intercept-console.ts index 01c4063e7a..565d475cae 100644 --- a/libs/common/spec/intercept-console.ts +++ b/libs/common/spec/intercept-console.ts @@ -2,22 +2,17 @@ const originalConsole = console; declare let console: any; -export function interceptConsole(interceptions: any): object { +export function interceptConsole(): { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; +} { console = { - log: function () { - // eslint-disable-next-line - interceptions.log = arguments; - }, - warn: function () { - // eslint-disable-next-line - interceptions.warn = arguments; - }, - error: function () { - // eslint-disable-next-line - interceptions.error = arguments; - }, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), }; - return interceptions; + return console; } export function restoreConsole() { diff --git a/libs/common/spec/observable-tracker.ts b/libs/common/spec/observable-tracker.ts index a6f3e6a879..16fad869c3 100644 --- a/libs/common/spec/observable-tracker.ts +++ b/libs/common/spec/observable-tracker.ts @@ -16,9 +16,11 @@ export class ObservableTracker { /** * Awaits the next emission from the observable, or throws if the timeout is exceeded * @param msTimeout The maximum time to wait for another emission before throwing + * @returns The next emission from the observable + * @throws If the timeout is exceeded */ - async expectEmission(msTimeout = 50) { - await firstValueFrom( + async expectEmission(msTimeout = 50): Promise { + return await firstValueFrom( this.observable.pipe( timeout({ first: msTimeout, @@ -28,7 +30,7 @@ export class ObservableTracker { ); } - /** Awaits until the the total number of emissions observed by this tracker equals or exceeds {@link count} + /** Awaits until the total number of emissions observed by this tracker equals or exceeds {@link count} * @param count The number of emissions to wait for */ async pauseUntilReceived(count: number, msTimeout = 50): Promise { diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index 9b3160ee19..c1a0e1f9cd 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -103,6 +103,7 @@ import { EventResponse } from "../models/response/event.response"; import { ListResponse } from "../models/response/list.response"; import { ProfileResponse } from "../models/response/profile.response"; import { UserKeyResponse } from "../models/response/user-key.response"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -451,7 +452,13 @@ export abstract class ApiService { end: string, token: string, ) => Promise>; - postEventsCollect: (request: EventRequest[]) => Promise; + + /** + * Posts events for a user + * @param request The array of events to upload + * @param userId The optional user id the events belong to. If no user id is provided the active user id is used. + */ + postEventsCollect: (request: EventRequest[], userId?: UserId) => Promise; deleteSsoUser: (organizationId: string) => Promise; getSsoUserIdentifier: () => Promise; diff --git a/libs/common/src/admin-console/abstractions/provider.service.ts b/libs/common/src/admin-console/abstractions/provider.service.ts index eb5e347eda..084e0a0c65 100644 --- a/libs/common/src/admin-console/abstractions/provider.service.ts +++ b/libs/common/src/admin-console/abstractions/provider.service.ts @@ -1,8 +1,11 @@ +import { Observable } from "rxjs"; + import { UserId } from "../../types/guid"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; export abstract class ProviderService { + get$: (id: string) => Observable; get: (id: string) => Promise; getAll: () => Promise; save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise; diff --git a/libs/common/src/admin-console/enums/index.ts b/libs/common/src/admin-console/enums/index.ts index 0cbdf65805..83b8a941a0 100644 --- a/libs/common/src/admin-console/enums/index.ts +++ b/libs/common/src/admin-console/enums/index.ts @@ -7,3 +7,4 @@ export * from "./provider-type.enum"; export * from "./provider-user-status-type.enum"; export * from "./provider-user-type.enum"; export * from "./scim-provider-type.enum"; +export * from "./provider-status-type.enum"; diff --git a/libs/common/src/admin-console/enums/provider-status-type.enum.ts b/libs/common/src/admin-console/enums/provider-status-type.enum.ts new file mode 100644 index 0000000000..8da60af0eb --- /dev/null +++ b/libs/common/src/admin-console/enums/provider-status-type.enum.ts @@ -0,0 +1,5 @@ +export enum ProviderStatusType { + Pending = 0, + Created = 1, + Billable = 2, +} diff --git a/libs/common/src/admin-console/models/data/provider.data.ts b/libs/common/src/admin-console/models/data/provider.data.ts index a848888025..ff060ae270 100644 --- a/libs/common/src/admin-console/models/data/provider.data.ts +++ b/libs/common/src/admin-console/models/data/provider.data.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProfileProviderResponse } from "../response/profile-provider.response"; export class ProviderData { @@ -9,6 +9,7 @@ export class ProviderData { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: ProfileProviderResponse) { this.id = response.id; @@ -18,5 +19,6 @@ export class ProviderData { this.enabled = response.enabled; this.userId = response.userId; this.useEvents = response.useEvents; + this.providerStatus = response.providerStatus; } } diff --git a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts index 24f6d0e9c7..470fa2317e 100644 --- a/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts +++ b/libs/common/src/admin-console/models/domain/encrypted-organization-key.ts @@ -25,7 +25,13 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey { constructor(private key: string) {} async decrypt(cryptoService: CryptoService) { - const decValue = await cryptoService.rsaDecrypt(this.key); + const activeUserPrivateKey = await cryptoService.getPrivateKey(); + + if (activeUserPrivateKey == null) { + throw new Error("Active user does not have a private key, cannot decrypt organization key."); + } + + const decValue = await cryptoService.rsaDecrypt(this.key, activeUserPrivateKey); return new SymmetricCryptoKey(decValue) as OrgKey; } diff --git a/libs/common/src/admin-console/models/domain/organization.ts b/libs/common/src/admin-console/models/domain/organization.ts index bdf0b8fbbf..04840477df 100644 --- a/libs/common/src/admin-console/models/domain/organization.ts +++ b/libs/common/src/admin-console/models/domain/organization.ts @@ -203,22 +203,32 @@ export class Organization { ); } - canEditUnassignedCiphers() { - // TODO: Update this to exclude Providers if provider access is restricted in AC-1707 + canEditUnassignedCiphers(restrictProviderAccessFlagEnabled: boolean) { + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } return this.isAdmin || this.permissions.editAnyCollection; } - canEditAllCiphers(flexibleCollectionsV1Enabled: boolean) { + canEditAllCiphers( + flexibleCollectionsV1Enabled: boolean, + restrictProviderAccessFlagEnabled: boolean, + ) { // Before Flexible Collections, any admin or anyone with editAnyCollection permission could edit all ciphers - if (!this.flexibleCollections || !flexibleCollectionsV1Enabled) { + if (!this.flexibleCollections || !flexibleCollectionsV1Enabled || !this.flexibleCollections) { return this.isAdmin || this.permissions.editAnyCollection; } + + if (this.isProviderUser) { + return !restrictProviderAccessFlagEnabled; + } + // Post Flexible Collections V1, the allowAdminAccessToAllCollectionItems flag can restrict admins - // Providers and custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag + // Custom users with canEditAnyCollection are not affected by allowAdminAccessToAllCollectionItems flag return ( - this.isProviderUser || (this.type === OrganizationUserType.Custom && this.permissions.editAnyCollection) || - (this.allowAdminAccessToAllCollectionItems && this.isAdmin) + (this.allowAdminAccessToAllCollectionItems && + (this.type === OrganizationUserType.Admin || this.type === OrganizationUserType.Owner)) ); } diff --git a/libs/common/src/admin-console/models/domain/provider.ts b/libs/common/src/admin-console/models/domain/provider.ts index d6d3d3c462..d51f698547 100644 --- a/libs/common/src/admin-console/models/domain/provider.ts +++ b/libs/common/src/admin-console/models/domain/provider.ts @@ -1,4 +1,4 @@ -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { ProviderData } from "../data/provider.data"; export class Provider { @@ -9,6 +9,7 @@ export class Provider { enabled: boolean; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(obj?: ProviderData) { if (obj == null) { @@ -22,6 +23,7 @@ export class Provider { this.enabled = obj.enabled; this.userId = obj.userId; this.useEvents = obj.useEvents; + this.providerStatus = obj.providerStatus; } get canAccess() { diff --git a/libs/common/src/admin-console/models/response/profile-provider.response.ts b/libs/common/src/admin-console/models/response/profile-provider.response.ts index eaecc9b847..701fe843de 100644 --- a/libs/common/src/admin-console/models/response/profile-provider.response.ts +++ b/libs/common/src/admin-console/models/response/profile-provider.response.ts @@ -1,5 +1,5 @@ import { BaseResponse } from "../../../models/response/base.response"; -import { ProviderUserStatusType, ProviderUserType } from "../../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../../enums"; import { PermissionsApi } from "../api/permissions.api"; export class ProfileProviderResponse extends BaseResponse { @@ -12,6 +12,7 @@ export class ProfileProviderResponse extends BaseResponse { permissions: PermissionsApi; userId: string; useEvents: boolean; + providerStatus: ProviderStatusType; constructor(response: any) { super(response); @@ -24,5 +25,6 @@ export class ProfileProviderResponse extends BaseResponse { this.permissions = new PermissionsApi(this.getResponseProperty("permissions")); this.userId = this.getResponseProperty("UserId"); this.useEvents = this.getResponseProperty("UseEvents"); + this.providerStatus = this.getResponseProperty("ProviderStatus"); } } diff --git a/libs/common/src/admin-console/models/response/provider/provider.response.ts b/libs/common/src/admin-console/models/response/provider/provider.response.ts index 0ea925cd33..369499595c 100644 --- a/libs/common/src/admin-console/models/response/provider/provider.response.ts +++ b/libs/common/src/admin-console/models/response/provider/provider.response.ts @@ -1,4 +1,5 @@ import { BaseResponse } from "../../../../models/response/base.response"; +import { ProviderType } from "../../../enums"; export class ProviderResponse extends BaseResponse { id: string; @@ -6,6 +7,7 @@ export class ProviderResponse extends BaseResponse { businessName: string; billingEmail: string; creationDate: Date; + type: ProviderType; constructor(response: any) { super(response); @@ -14,5 +16,6 @@ export class ProviderResponse extends BaseResponse { this.businessName = this.getResponseProperty("BusinessName"); this.billingEmail = this.getResponseProperty("BillingEmail"); this.creationDate = this.getResponseProperty("CreationDate"); + this.type = this.getResponseProperty("Type"); } } diff --git a/libs/common/src/admin-console/services/provider.service.spec.ts b/libs/common/src/admin-console/services/provider.service.spec.ts index fcba9d5023..0f4414804b 100644 --- a/libs/common/src/admin-console/services/provider.service.spec.ts +++ b/libs/common/src/admin-console/services/provider.service.spec.ts @@ -1,8 +1,10 @@ +import { firstValueFrom } from "rxjs"; + import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { ProviderUserStatusType, ProviderUserType } from "../enums"; +import { ProviderStatusType, ProviderUserStatusType, ProviderUserType } from "../enums"; import { ProviderData } from "../models/data/provider.data"; import { Provider } from "../models/domain/provider"; @@ -64,6 +66,7 @@ describe("PROVIDERS key definition", () => { enabled: true, userId: "string", useEvents: true, + providerStatus: ProviderStatusType.Pending, }, }; const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult))); @@ -85,6 +88,7 @@ describe("ProviderService", () => { fakeStateProvider = new FakeStateProvider(fakeAccountService); fakeUserState = fakeStateProvider.singleUser.getFake(fakeUserId, PROVIDERS); fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS); + providerService = new ProviderService(fakeStateProvider); }); @@ -105,6 +109,22 @@ describe("ProviderService", () => { }); }); + describe("get$()", () => { + it("Returns an observable of a single provider from state that matches the specified id", async () => { + const mockData = buildMockProviders(5); + fakeUserState.nextState(arrayToRecord(mockData)); + const result = providerService.get$(mockData[3].id); + const provider = await firstValueFrom(result); + expect(provider).toEqual(new Provider(mockData[3])); + }); + + it("Returns an observable of undefined if the specified provider is not found", async () => { + const result = providerService.get$("this-provider-does-not-exist"); + const provider = await firstValueFrom(result); + expect(provider).toBe(undefined); + }); + }); + describe("get()", () => { it("Returns a single provider from state that matches the specified id", async () => { const mockData = buildMockProviders(5); diff --git a/libs/common/src/admin-console/services/provider.service.ts b/libs/common/src/admin-console/services/provider.service.ts index 064e0c7175..4a462316b9 100644 --- a/libs/common/src/admin-console/services/provider.service.ts +++ b/libs/common/src/admin-console/services/provider.service.ts @@ -1,6 +1,6 @@ -import { Observable, map, firstValueFrom, of, switchMap, take } from "rxjs"; +import { firstValueFrom, map, Observable, of, switchMap, take } from "rxjs"; -import { UserKeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state"; +import { PROVIDERS_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service"; import { ProviderData } from "../models/data/provider.data"; @@ -38,6 +38,10 @@ export class ProviderService implements ProviderServiceAbstraction { ); } + get$(id: string): Observable { + return this.providers$().pipe(mapToSingleProvider(id)); + } + async get(id: string): Promise { return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id))); } diff --git a/libs/common/src/auth/abstractions/account.service.ts b/libs/common/src/auth/abstractions/account.service.ts index fa9ad36378..b7fd6d9bb9 100644 --- a/libs/common/src/auth/abstractions/account.service.ts +++ b/libs/common/src/auth/abstractions/account.service.ts @@ -8,18 +8,44 @@ import { UserId } from "../../types/guid"; */ export type AccountInfo = { email: string; + emailVerified: boolean; name: string | undefined; }; export function accountInfoEqual(a: AccountInfo, b: AccountInfo) { - return a?.email === b?.email && a?.name === b?.name; + if (a == null && b == null) { + return true; + } + + if (a == null || b == null) { + return false; + } + + const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set; + for (const key of keys) { + if (a[key] !== b[key]) { + return false; + } + } + return true; } export abstract class AccountService { accounts$: Observable>; activeAccount$: Observable<{ id: UserId | undefined } & AccountInfo>; + + /** + * Observable of the last activity time for each account. + */ + accountActivity$: Observable>; + /** Account list in order of descending recency */ + sortedUserIds$: Observable; + /** Next account that is not the current active account */ + nextUpAccount$: Observable<{ id: UserId } & AccountInfo>; /** * Updates the `accounts$` observable with the new account data. + * + * @note Also sets the last active date of the account to `now`. * @param userId * @param accountData */ @@ -36,11 +62,30 @@ export abstract class AccountService { * @param email */ abstract setAccountEmail(userId: UserId, email: string): Promise; + /** + * updates the `accounts$` observable with the new email verification status for the account. + * @param userId + * @param emailVerified + */ + abstract setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise; /** * Updates the `activeAccount$` observable with the new active account. * @param userId */ abstract switchAccount(userId: UserId): Promise; + /** + * Cleans personal information for the given account from the `accounts$` observable. Does not remove the userId from the observable. + * + * @note Also sets the last active date of the account to `null`. + * @param userId + */ + abstract clean(userId: UserId): Promise; + /** + * Updates the given user's last activity time. + * @param userId + * @param lastActivity + */ + abstract setAccountActivity(userId: UserId, lastActivity: Date): Promise; } export abstract class InternalAccountService extends AccountService { diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts similarity index 89% rename from libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts rename to libs/common/src/auth/abstractions/device-trust.service.abstraction.ts index 53fe214035..123f710338 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust.service.abstraction.ts @@ -3,9 +3,10 @@ import { Observable } from "rxjs"; import { EncString } from "../../platform/models/domain/enc-string"; import { UserId } from "../../types/guid"; import { DeviceKey, UserKey } from "../../types/key"; -import { DeviceResponse } from "../abstractions/devices/responses/device.response"; -export abstract class DeviceTrustCryptoServiceAbstraction { +import { DeviceResponse } from "./devices/responses/device.response"; + +export abstract class DeviceTrustServiceAbstraction { supportsDeviceTrust$: Observable; /** * @description Retrieves the users choice to trust the device which can only happen after decryption diff --git a/libs/common/src/auth/abstractions/kdf-config.service.ts b/libs/common/src/auth/abstractions/kdf-config.service.ts new file mode 100644 index 0000000000..6b41979e1b --- /dev/null +++ b/libs/common/src/auth/abstractions/kdf-config.service.ts @@ -0,0 +1,7 @@ +import { UserId } from "../../types/guid"; +import { KdfConfig } from "../models/domain/kdf-config"; + +export abstract class KdfConfigService { + setKdfConfig: (userId: UserId, KdfConfig: KdfConfig) => Promise; + getKdfConfig: () => Promise; +} diff --git a/libs/common/src/auth/abstractions/key-connector.service.ts b/libs/common/src/auth/abstractions/key-connector.service.ts index 36f413d70c..b1b6727cd1 100644 --- a/libs/common/src/auth/abstractions/key-connector.service.ts +++ b/libs/common/src/auth/abstractions/key-connector.service.ts @@ -1,8 +1,9 @@ import { Organization } from "../../admin-console/models/domain/organization"; +import { UserId } from "../../types/guid"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; export abstract class KeyConnectorService { - setMasterKeyFromUrl: (url?: string) => Promise; + setMasterKeyFromUrl: (url: string, userId: UserId) => Promise; getManagingOrganization: () => Promise; getUsesKeyConnector: () => Promise; migrateUser: () => Promise; @@ -10,6 +11,7 @@ export abstract class KeyConnectorService { convertNewSsoUserToKeyConnector: ( tokenResponse: IdentityTokenResponse, orgId: string, + userId: UserId, ) => Promise; setUsesKeyConnector: (enabled: boolean) => Promise; setConvertAccountRequired: (status: boolean) => Promise; diff --git a/libs/common/src/auth/abstractions/token.service.ts b/libs/common/src/auth/abstractions/token.service.ts index 75bb383882..fc3bd317f4 100644 --- a/libs/common/src/auth/abstractions/token.service.ts +++ b/libs/common/src/auth/abstractions/token.service.ts @@ -213,4 +213,10 @@ export abstract class TokenService { * @returns A promise that resolves with a boolean representing the user's external authN status. */ getIsExternal: () => Promise; + + /** Gets the active or passed in user's security stamp */ + getSecurityStamp: (userId?: UserId) => Promise; + + /** Sets the security stamp for the active or passed in user */ + setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise; } diff --git a/libs/common/src/auth/abstractions/two-factor.service.ts b/libs/common/src/auth/abstractions/two-factor.service.ts index 3ea7eb8db9..a0a9ecd2af 100644 --- a/libs/common/src/auth/abstractions/two-factor.service.ts +++ b/libs/common/src/auth/abstractions/two-factor.service.ts @@ -12,12 +12,12 @@ export interface TwoFactorProviderDetails { export abstract class TwoFactorService { init: () => void; - getSupportedProviders: (win: Window) => TwoFactorProviderDetails[]; - getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType; - setSelectedProvider: (type: TwoFactorProviderType) => void; - clearSelectedProvider: () => void; + getSupportedProviders: (win: Window) => Promise; + getDefaultProvider: (webAuthnSupported: boolean) => Promise; + setSelectedProvider: (type: TwoFactorProviderType) => Promise; + clearSelectedProvider: () => Promise; - setProviders: (response: IdentityTwoFactorResponse) => void; - clearProviders: () => void; - getProviders: () => Map; + setProviders: (response: IdentityTwoFactorResponse) => Promise; + clearProviders: () => Promise; + getProviders: () => Promise>; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 993ce08d58..bc828d3e86 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -1,9 +1,11 @@ import { Utils } from "../../../platform/misc/utils"; +import { UserId } from "../../../types/guid"; import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; import { ForceSetPasswordReason } from "./force-set-password-reason"; export class AuthResult { + userId: UserId; captchaSiteKey = ""; // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal /** @@ -14,7 +16,7 @@ export class AuthResult { resetMasterPassword = false; forcePasswordReset: ForceSetPasswordReason = ForceSetPasswordReason.None; - twoFactorProviders: Map = null; + twoFactorProviders: Partial>> = null; ssoEmail2FaSessionToken?: string; email: string; requiresEncryptionKeyMigration: boolean; diff --git a/libs/common/src/auth/models/domain/kdf-config.ts b/libs/common/src/auth/models/domain/kdf-config.ts index a25ba586e9..ce01f09702 100644 --- a/libs/common/src/auth/models/domain/kdf-config.ts +++ b/libs/common/src/auth/models/domain/kdf-config.ts @@ -1,11 +1,86 @@ -export class KdfConfig { - iterations: number; - memory?: number; - parallelism?: number; +import { Jsonify } from "type-fest"; - constructor(iterations: number, memory?: number, parallelism?: number) { - this.iterations = iterations; - this.memory = memory; - this.parallelism = parallelism; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + KdfType, + PBKDF2_ITERATIONS, +} from "../../../platform/enums/kdf-type.enum"; + +/** + * Represents a type safe KDF configuration. + */ +export type KdfConfig = PBKDF2KdfConfig | Argon2KdfConfig; + +/** + * Password-Based Key Derivation Function 2 (PBKDF2) KDF configuration. + */ +export class PBKDF2KdfConfig { + kdfType: KdfType.PBKDF2_SHA256 = KdfType.PBKDF2_SHA256; + iterations: number; + + constructor(iterations?: number) { + this.iterations = iterations ?? PBKDF2_ITERATIONS.defaultValue; + } + + /** + * Validates the PBKDF2 KDF configuration. + * A Valid PBKDF2 KDF configuration has KDF iterations between the 600_000 and 2_000_000. + */ + validateKdfConfig(): void { + if (!PBKDF2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + } + } + + static fromJSON(json: Jsonify): PBKDF2KdfConfig { + return new PBKDF2KdfConfig(json.iterations); + } +} + +/** + * Argon2 KDF configuration. + */ +export class Argon2KdfConfig { + kdfType: KdfType.Argon2id = KdfType.Argon2id; + iterations: number; + memory: number; + parallelism: number; + + constructor(iterations?: number, memory?: number, parallelism?: number) { + this.iterations = iterations ?? ARGON2_ITERATIONS.defaultValue; + this.memory = memory ?? ARGON2_MEMORY.defaultValue; + this.parallelism = parallelism ?? ARGON2_PARALLELISM.defaultValue; + } + + /** + * Validates the Argon2 KDF configuration. + * A Valid Argon2 KDF configuration has iterations between 2 and 10, memory between 16mb and 1024mb, and parallelism between 1 and 16. + */ + validateKdfConfig(): void { + if (!ARGON2_ITERATIONS.inRange(this.iterations)) { + throw new Error( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + } + + if (!ARGON2_MEMORY.inRange(this.memory)) { + throw new Error( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + } + + if (!ARGON2_PARALLELISM.inRange(this.parallelism)) { + throw new Error( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, + ); + } + } + + static fromJSON(json: Jsonify): Argon2KdfConfig { + return new Argon2KdfConfig(json.iterations, json.memory, json.parallelism); } } diff --git a/libs/common/src/auth/models/request/set-key-connector-key.request.ts b/libs/common/src/auth/models/request/set-key-connector-key.request.ts index dfd32689d8..c8081bdec2 100644 --- a/libs/common/src/auth/models/request/set-key-connector-key.request.ts +++ b/libs/common/src/auth/models/request/set-key-connector-key.request.ts @@ -11,18 +11,14 @@ export class SetKeyConnectorKeyRequest { kdfParallelism?: number; orgIdentifier: string; - constructor( - key: string, - kdf: KdfType, - kdfConfig: KdfConfig, - orgIdentifier: string, - keys: KeysRequest, - ) { + constructor(key: string, kdfConfig: KdfConfig, orgIdentifier: string, keys: KeysRequest) { this.key = key; - this.kdf = kdf; + this.kdf = kdfConfig.kdfType; this.kdfIterations = kdfConfig.iterations; - this.kdfMemory = kdfConfig.memory; - this.kdfParallelism = kdfConfig.parallelism; + if (kdfConfig.kdfType === KdfType.Argon2id) { + this.kdfMemory = kdfConfig.memory; + this.kdfParallelism = kdfConfig.parallelism; + } this.orgIdentifier = orgIdentifier; this.keys = keys; } diff --git a/libs/common/src/auth/models/response/identity-two-factor.response.ts b/libs/common/src/auth/models/response/identity-two-factor.response.ts index bc5d2fbf85..dce64e8ef3 100644 --- a/libs/common/src/auth/models/response/identity-two-factor.response.ts +++ b/libs/common/src/auth/models/response/identity-two-factor.response.ts @@ -4,8 +4,10 @@ import { TwoFactorProviderType } from "../../enums/two-factor-provider-type"; import { MasterPasswordPolicyResponse } from "./master-password-policy.response"; export class IdentityTwoFactorResponse extends BaseResponse { + // contains available two-factor providers twoFactorProviders: TwoFactorProviderType[]; - twoFactorProviders2 = new Map(); + // a map of two-factor providers to necessary data for completion + twoFactorProviders2: Record>; captchaToken: string; ssoEmail2faSessionToken: string; email?: string; @@ -15,15 +17,7 @@ export class IdentityTwoFactorResponse extends BaseResponse { super(response); this.captchaToken = this.getResponseProperty("CaptchaBypassToken"); this.twoFactorProviders = this.getResponseProperty("TwoFactorProviders"); - const twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); - if (twoFactorProviders2 != null) { - for (const prop in twoFactorProviders2) { - // eslint-disable-next-line - if (twoFactorProviders2.hasOwnProperty(prop)) { - this.twoFactorProviders2.set(parseInt(prop, null), twoFactorProviders2[prop]); - } - } - } + this.twoFactorProviders2 = this.getResponseProperty("TwoFactorProviders2"); this.masterPasswordPolicy = new MasterPasswordPolicyResponse( this.getResponseProperty("MasterPasswordPolicy"), ); diff --git a/libs/common/src/auth/services/account.service.spec.ts b/libs/common/src/auth/services/account.service.spec.ts index a9cec82c51..0ae14b0cc1 100644 --- a/libs/common/src/auth/services/account.service.spec.ts +++ b/libs/common/src/auth/services/account.service.spec.ts @@ -1,3 +1,8 @@ +/** + * need to update test environment so structuredClone works appropriately + * @jest-environment ../../libs/shared/test.environment.ts + */ + import { MockProxy, mock } from "jest-mock-extended"; import { firstValueFrom } from "rxjs"; @@ -6,15 +11,57 @@ import { FakeGlobalStateProvider } from "../../../spec/fake-state-provider"; import { trackEmissions } from "../../../spec/utils"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; -import { AccountInfo } from "../abstractions/account.service"; +import { AccountInfo, accountInfoEqual } from "../abstractions/account.service"; import { ACCOUNT_ACCOUNTS, ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, AccountServiceImplementation, } from "./account.service"; +describe("accountInfoEqual", () => { + const accountInfo: AccountInfo = { name: "name", email: "email", emailVerified: true }; + + it("compares nulls", () => { + expect(accountInfoEqual(null, null)).toBe(true); + expect(accountInfoEqual(null, accountInfo)).toBe(false); + expect(accountInfoEqual(accountInfo, null)).toBe(false); + }); + + it("compares all keys, not just those defined in AccountInfo", () => { + const different = { ...accountInfo, extra: "extra" }; + + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares name", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, name: "name2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares email", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, email: "email2" }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); + + it("compares emailVerified", () => { + const same = { ...accountInfo }; + const different = { ...accountInfo, emailVerified: false }; + + expect(accountInfoEqual(accountInfo, same)).toBe(true); + expect(accountInfoEqual(accountInfo, different)).toBe(false); + }); +}); + describe("accountService", () => { let messagingService: MockProxy; let logService: MockProxy; @@ -22,8 +69,8 @@ describe("accountService", () => { let sut: AccountServiceImplementation; let accountsState: FakeGlobalState>; let activeAccountIdState: FakeGlobalState; - const userId = "userId" as UserId; - const userInfo = { email: "email", name: "name" }; + const userId = Utils.newGuid() as UserId; + const userInfo = { email: "email", name: "name", emailVerified: true }; beforeEach(() => { messagingService = mock(); @@ -86,6 +133,25 @@ describe("accountService", () => { expect(currentValue).toEqual({ [userId]: userInfo }); }); + + it("sets the last active date of the account to now", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await sut.addAccount(userId, userInfo); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: expect.any(Date) }); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({}); + await expect(sut.addAccount(userId as UserId, userInfo)).rejects.toThrow( + "userId is required", + ); + }, + ); }); describe("setAccountName", () => { @@ -134,6 +200,58 @@ describe("accountService", () => { }); }); + describe("setAccountEmailVerified", () => { + const initialState = { [userId]: userInfo }; + initialState[userId].emailVerified = false; + beforeEach(() => { + accountsState.stateSubject.next(initialState); + }); + + it("should update the account", async () => { + await sut.setAccountEmailVerified(userId, true); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { ...userInfo, emailVerified: true }, + }); + }); + + it("should not update if the email is the same", async () => { + await sut.setAccountEmailVerified(userId, false); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual(initialState); + }); + }); + + describe("clean", () => { + beforeEach(() => { + accountsState.stateSubject.next({ [userId]: userInfo }); + }); + + it("removes account info of the given user", async () => { + await sut.clean(userId); + const currentState = await firstValueFrom(accountsState.state$); + + expect(currentState).toEqual({ + [userId]: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + }); + + it("removes account activity of the given user", async () => { + const state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + state.stateSubject.next({ [userId]: new Date() }); + + await sut.clean(userId); + + expect(state.nextMock).toHaveBeenCalledWith({}); + }); + }); + describe("switchAccount", () => { beforeEach(() => { accountsState.stateSubject.next({ [userId]: userInfo }); @@ -152,4 +270,83 @@ describe("accountService", () => { expect(sut.switchAccount("unknown" as UserId)).rejects.toThrowError("Account does not exist"); }); }); + + describe("account activity", () => { + let state: FakeGlobalState>; + + beforeEach(() => { + state = globalStateProvider.getFake(ACCOUNT_ACTIVITY); + }); + describe("accountActivity$", () => { + it("returns the account activity state", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({ + [toId("user1")]: new Date(1), + [toId("user2")]: new Date(2), + }); + }); + + it("returns an empty object when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.accountActivity$)).resolves.toEqual({}); + }); + }); + + describe("sortedUserIds$", () => { + it("returns the sorted user ids by date with most recent first", async () => { + state.stateSubject.next({ + [toId("user1")]: new Date(3), + [toId("user2")]: new Date(2), + [toId("user3")]: new Date(1), + }); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([ + "user1" as UserId, + "user2" as UserId, + "user3" as UserId, + ]); + }); + + it("returns an empty array when account activity is null", async () => { + state.stateSubject.next(null); + + await expect(firstValueFrom(sut.sortedUserIds$)).resolves.toEqual([]); + }); + }); + + describe("setAccountActivity", () => { + const userId = Utils.newGuid() as UserId; + it("sets the account activity", async () => { + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).toHaveBeenCalledWith({ [userId]: new Date(1) }); + }); + + it("does not update if the activity is the same", async () => { + state.stateSubject.next({ [userId]: new Date(1) }); + + await sut.setAccountActivity(userId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }); + + it.each([null, undefined, 123, "not a guid"])( + "does not set last active if the userId is not a valid guid", + async (userId) => { + await sut.setAccountActivity(userId as UserId, new Date(1)); + + expect(state.nextMock).not.toHaveBeenCalled(); + }, + ); + }); + }); }); + +function toId(userId: string) { + return userId as UserId; +} diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index 77d61fae91..6740387ded 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -1,4 +1,4 @@ -import { Subject, combineLatestWith, map, distinctUntilChanged, shareReplay } from "rxjs"; +import { combineLatestWith, map, distinctUntilChanged, shareReplay, combineLatest } from "rxjs"; import { AccountInfo, @@ -7,8 +7,9 @@ import { } from "../../auth/abstractions/account.service"; import { LogService } from "../../platform/abstractions/log.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; +import { Utils } from "../../platform/misc/utils"; import { - ACCOUNT_MEMORY, + ACCOUNT_DISK, GlobalState, GlobalStateProvider, KeyDefinition, @@ -16,25 +17,36 @@ import { import { UserId } from "../../types/guid"; export const ACCOUNT_ACCOUNTS = KeyDefinition.record( - ACCOUNT_MEMORY, + ACCOUNT_DISK, "accounts", { deserializer: (accountInfo) => accountInfo, }, ); -export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_MEMORY, "activeAccountId", { +export const ACCOUNT_ACTIVE_ACCOUNT_ID = new KeyDefinition(ACCOUNT_DISK, "activeAccountId", { deserializer: (id: UserId) => id, }); +export const ACCOUNT_ACTIVITY = KeyDefinition.record(ACCOUNT_DISK, "activity", { + deserializer: (activity) => new Date(activity), +}); + +const LOGGED_OUT_INFO: AccountInfo = { + email: "", + emailVerified: false, + name: undefined, +}; + export class AccountServiceImplementation implements InternalAccountService { - private lock = new Subject(); - private logout = new Subject(); private accountsState: GlobalState>; private activeAccountIdState: GlobalState; accounts$; activeAccount$; + accountActivity$; + sortedUserIds$; + nextUpAccount$; constructor( private messagingService: MessagingService, @@ -53,14 +65,40 @@ export class AccountServiceImplementation implements InternalAccountService { distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)), shareReplay({ bufferSize: 1, refCount: false }), ); + this.accountActivity$ = this.globalStateProvider + .get(ACCOUNT_ACTIVITY) + .state$.pipe(map((activity) => activity ?? {})); + this.sortedUserIds$ = this.accountActivity$.pipe( + map((activity) => { + return Object.entries(activity) + .map(([userId, lastActive]: [UserId, Date]) => ({ userId, lastActive })) + .sort((a, b) => b.lastActive.getTime() - a.lastActive.getTime()) // later dates first + .map((a) => a.userId); + }), + ); + this.nextUpAccount$ = combineLatest([ + this.accounts$, + this.activeAccount$, + this.sortedUserIds$, + ]).pipe( + map(([accounts, activeAccount, sortedUserIds]) => { + const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null); + return nextId ? { id: nextId, ...accounts[nextId] } : null; + }), + ); } async addAccount(userId: UserId, accountData: AccountInfo): Promise { + if (!Utils.isGuid(userId)) { + throw new Error("userId is required"); + } + await this.accountsState.update((accounts) => { accounts ||= {}; accounts[userId] = accountData; return accounts; }); + await this.setAccountActivity(userId, new Date()); } async setAccountName(userId: UserId, name: string): Promise { @@ -71,6 +109,15 @@ export class AccountServiceImplementation implements InternalAccountService { await this.setAccountInfo(userId, { email }); } + async setAccountEmailVerified(userId: UserId, emailVerified: boolean): Promise { + await this.setAccountInfo(userId, { emailVerified }); + } + + async clean(userId: UserId) { + await this.setAccountInfo(userId, LOGGED_OUT_INFO); + await this.removeAccountActivity(userId); + } + async switchAccount(userId: UserId): Promise { await this.activeAccountIdState.update( (_, accounts) => { @@ -94,6 +141,37 @@ export class AccountServiceImplementation implements InternalAccountService { ); } + async setAccountActivity(userId: UserId, lastActivity: Date): Promise { + if (!Utils.isGuid(userId)) { + // only store for valid userIds + return; + } + + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + activity ||= {}; + activity[userId] = lastActivity; + return activity; + }, + { + shouldUpdate: (oldActivity) => oldActivity?.[userId]?.getTime() !== lastActivity?.getTime(), + }, + ); + } + + async removeAccountActivity(userId: UserId): Promise { + await this.globalStateProvider.get(ACCOUNT_ACTIVITY).update( + (activity) => { + if (activity == null) { + return activity; + } + delete activity[userId]; + return activity; + }, + { shouldUpdate: (oldActivity) => oldActivity?.[userId] != null }, + ); + } + // TODO: update to use our own account status settings. Requires inverting direction of state service accounts flow async delete(): Promise { try { diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts index 3bdf85d3e1..9a93a4207b 100644 --- a/libs/common/src/auth/services/auth.service.spec.ts +++ b/libs/common/src/auth/services/auth.service.spec.ts @@ -56,6 +56,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: userId, email: "email", + emailVerified: false, name: "name", }; @@ -109,6 +110,7 @@ describe("AuthService", () => { status: AuthenticationStatus.Unlocked, id: Utils.newGuid() as UserId, email: "email2", + emailVerified: false, name: "name2", }; @@ -126,7 +128,11 @@ describe("AuthService", () => { it("requests auth status for all known users", async () => { const userId2 = Utils.newGuid() as UserId; - await accountService.addAccount(userId2, { email: "email2", name: "name2" }); + await accountService.addAccount(userId2, { + email: "email2", + emailVerified: false, + name: "name2", + }); const mockFn = jest.fn().mockReturnValue(of(AuthenticationStatus.Locked)); sut.authStatusFor$ = mockFn; @@ -147,11 +153,14 @@ describe("AuthService", () => { cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined)); }); - it("emits LoggedOut when userId is null", async () => { - expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( - AuthenticationStatus.LoggedOut, - ); - }); + it.each([null, undefined, "not a userId"])( + "emits LoggedOut when userId is invalid (%s)", + async () => { + expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual( + AuthenticationStatus.LoggedOut, + ); + }, + ); it("emits LoggedOut when there is no access token", async () => { tokenService.hasAccessToken$.mockReturnValue(of(false)); diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 7a29d313e7..a4529084a2 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -2,6 +2,7 @@ import { Observable, combineLatest, distinctUntilChanged, + firstValueFrom, map, of, shareReplay, @@ -12,7 +13,7 @@ import { ApiService } from "../../abstractions/api.service"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { StateService } from "../../platform/abstractions/state.service"; -import { KeySuffixOptions } from "../../platform/enums"; +import { Utils } from "../../platform/misc/utils"; import { UserId } from "../../types/guid"; import { AccountService } from "../abstractions/account.service"; import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service"; @@ -40,13 +41,16 @@ export class AuthService implements AuthServiceAbstraction { this.authStatuses$ = this.accountService.accounts$.pipe( map((accounts) => Object.keys(accounts) as UserId[]), - switchMap((entries) => - combineLatest( + switchMap((entries) => { + if (entries.length === 0) { + return of([] as { userId: UserId; status: AuthenticationStatus }[]); + } + return combineLatest( entries.map((userId) => this.authStatusFor$(userId).pipe(map((status) => ({ userId, status }))), ), - ), - ), + ); + }), map((statuses) => { return statuses.reduce( (acc, { userId, status }) => { @@ -60,7 +64,7 @@ export class AuthService implements AuthServiceAbstraction { } authStatusFor$(userId: UserId): Observable { - if (userId == null) { + if (!Utils.isGuid(userId)) { return of(AuthenticationStatus.LoggedOut); } @@ -85,37 +89,8 @@ export class AuthService implements AuthServiceAbstraction { } async getAuthStatus(userId?: string): Promise { - // If we don't have an access token or userId, we're logged out - const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId }); - if (!isAuthenticated) { - return AuthenticationStatus.LoggedOut; - } - - // If we don't have a user key in memory, we're locked - if (!(await this.cryptoService.hasUserKeyInMemory(userId))) { - // Check if the user has vault timeout set to never and verify that - // they've never unlocked their vault - const neverLock = - (await this.cryptoService.hasUserKeyStored(KeySuffixOptions.Auto, userId)) && - !(await this.stateService.getEverBeenUnlocked({ userId: userId })); - - if (neverLock) { - // Attempt to get the key from storage and set it in memory - const userKey = await this.cryptoService.getUserKeyFromStorage( - KeySuffixOptions.Auto, - userId, - ); - await this.cryptoService.setUserKey(userKey, userId); - } - } - - // We do another check here in case setting the auto key failed - const hasKeyInMemory = await this.cryptoService.hasUserKeyInMemory(userId); - if (!hasKeyInMemory) { - return AuthenticationStatus.Locked; - } - - return AuthenticationStatus.Unlocked; + userId ??= await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + return await firstValueFrom(this.authStatusFor$(userId as UserId)); } logOut(callback: () => void) { diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts similarity index 88% rename from libs/common/src/auth/services/device-trust-crypto.service.implementation.ts rename to libs/common/src/auth/services/device-trust.service.implementation.ts index 6fb58eab28..dd98ce2b44 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust.service.implementation.ts @@ -8,6 +8,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; @@ -17,7 +18,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt import { DEVICE_TRUST_DISK_LOCAL, StateProvider, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { UserKey, DeviceKey } from "../../types/key"; -import { DeviceTrustCryptoServiceAbstraction } from "../abstractions/device-trust-crypto.service.abstraction"; +import { DeviceTrustServiceAbstraction } from "../abstractions/device-trust.service.abstraction"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; import { DevicesApiServiceAbstraction } from "../abstractions/devices-api.service.abstraction"; import { SecretVerificationRequest } from "../models/request/secret-verification.request"; @@ -42,7 +43,7 @@ export const SHOULD_TRUST_DEVICE = new UserKeyDefinition( }, ); -export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { +export class DeviceTrustService implements DeviceTrustServiceAbstraction { private readonly platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); private readonly deviceKeySecureStorageKey: string = "_deviceKey"; @@ -61,6 +62,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac private stateProvider: StateProvider, private secureStorageService: AbstractStorageService, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + private logService: LogService, ) { this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( map((options) => options?.trustedDeviceOption != null ?? false), @@ -110,7 +112,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac } // Attempt to get user key - const userKey: UserKey = await this.cryptoService.getUserKey(); + const userKey: UserKey = await this.cryptoService.getUserKey(userId); // If user key is not found, throw error if (!userKey) { @@ -223,19 +225,23 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac throw new Error("UserId is required. Cannot get device key."); } - if (this.platformSupportsSecureStorage) { - const deviceKeyB64 = await this.secureStorageService.get< - ReturnType - >(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); + try { + if (this.platformSupportsSecureStorage) { + const deviceKeyB64 = await this.secureStorageService.get< + ReturnType + >(`${userId}${this.deviceKeySecureStorageKey}`, this.getSecureStorageOptions(userId)); - const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey; + const deviceKey = SymmetricCryptoKey.fromJSON(deviceKeyB64) as DeviceKey; + + return deviceKey; + } + + const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId)); return deviceKey; + } catch (e) { + this.logService.error("Failed to get device key", e); } - - const deviceKey = await firstValueFrom(this.stateProvider.getUserState$(DEVICE_KEY, userId)); - - return deviceKey; } private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise { @@ -243,16 +249,20 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac throw new Error("UserId is required. Cannot set device key."); } - if (this.platformSupportsSecureStorage) { - await this.secureStorageService.save( - `${userId}${this.deviceKeySecureStorageKey}`, - deviceKey, - this.getSecureStorageOptions(userId), - ); - return; - } + try { + if (this.platformSupportsSecureStorage) { + await this.secureStorageService.save( + `${userId}${this.deviceKeySecureStorageKey}`, + deviceKey, + this.getSecureStorageOptions(userId), + ); + return; + } - await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); + await this.stateProvider.setUserState(DEVICE_KEY, deviceKey?.toJSON(), userId); + } catch (e) { + this.logService.error("Failed to set device key", e); + } } private async makeDeviceKey(): Promise { @@ -293,6 +303,7 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac return new SymmetricCryptoKey(userKey) as UserKey; } 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."); await this.setDeviceKey(userId, null); return null; diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts similarity index 86% rename from libs/common/src/auth/services/device-trust-crypto.service.spec.ts rename to libs/common/src/auth/services/device-trust.service.spec.ts index af147b3481..f61bce563f 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust.service.spec.ts @@ -14,6 +14,7 @@ import { CryptoService } from "../../platform/abstractions/crypto.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; +import { LogService } from "../../platform/abstractions/log.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { AbstractStorageService } from "../../platform/abstractions/storage.service"; import { StorageLocation } from "../../platform/enums"; @@ -33,11 +34,11 @@ import { ProtectedDeviceResponse } from "../models/response/protected-device.res import { SHOULD_TRUST_DEVICE, DEVICE_KEY, - DeviceTrustCryptoService, -} from "./device-trust-crypto.service.implementation"; + DeviceTrustService, +} from "./device-trust.service.implementation"; -describe("deviceTrustCryptoService", () => { - let deviceTrustCryptoService: DeviceTrustCryptoService; +describe("deviceTrustService", () => { + let deviceTrustService: DeviceTrustService; const keyGenerationService = mock(); const cryptoFunctionService = mock(); @@ -48,6 +49,7 @@ describe("deviceTrustCryptoService", () => { const i18nService = mock(); const platformUtilsService = mock(); const secureStorageService = mock(); + const logService = mock(); const userDecryptionOptionsService = mock(); const decryptionOptions = new BehaviorSubject(null); @@ -70,11 +72,11 @@ describe("deviceTrustCryptoService", () => { jest.clearAllMocks(); const supportsSecureStorage = false; // default to false; tests will override as needed // By default all the tests will have a mocked active user in state provider. - deviceTrustCryptoService = createDeviceTrustCryptoService(mockUserId, supportsSecureStorage); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("instantiates", () => { - expect(deviceTrustCryptoService).not.toBeFalsy(); + expect(deviceTrustService).not.toBeFalsy(); }); describe("User Trust Device Choice For Decryption", () => { @@ -84,7 +86,7 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, newValue, mockUserId); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); @@ -95,9 +97,9 @@ describe("deviceTrustCryptoService", () => { await stateProvider.setUserState(SHOULD_TRUST_DEVICE, false, mockUserId); const newValue = true; - await deviceTrustCryptoService.setShouldTrustDevice(mockUserId, newValue); + await deviceTrustService.setShouldTrustDevice(mockUserId, newValue); - const result = await deviceTrustCryptoService.getShouldTrustDevice(mockUserId); + const result = await deviceTrustService.getShouldTrustDevice(mockUserId); expect(result).toEqual(newValue); }); }); @@ -105,25 +107,25 @@ describe("deviceTrustCryptoService", () => { describe("trustDeviceIfRequired", () => { it("should trust device and reset when getShouldTrustDevice returns true", async () => { - jest.spyOn(deviceTrustCryptoService, "getShouldTrustDevice").mockResolvedValue(true); - jest.spyOn(deviceTrustCryptoService, "trustDevice").mockResolvedValue({} as DeviceResponse); - jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice").mockResolvedValue(); + jest.spyOn(deviceTrustService, "getShouldTrustDevice").mockResolvedValue(true); + jest.spyOn(deviceTrustService, "trustDevice").mockResolvedValue({} as DeviceResponse); + jest.spyOn(deviceTrustService, "setShouldTrustDevice").mockResolvedValue(); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); - expect(deviceTrustCryptoService.getShouldTrustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.trustDevice).toHaveBeenCalledTimes(1); - expect(deviceTrustCryptoService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); + expect(deviceTrustService.getShouldTrustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.trustDevice).toHaveBeenCalledTimes(1); + expect(deviceTrustService.setShouldTrustDevice).toHaveBeenCalledWith(mockUserId, false); }); it("should not trust device nor reset when getShouldTrustDevice returns false", async () => { const getShouldTrustDeviceSpy = jest - .spyOn(deviceTrustCryptoService, "getShouldTrustDevice") + .spyOn(deviceTrustService, "getShouldTrustDevice") .mockResolvedValue(false); - const trustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "trustDevice"); - const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustCryptoService, "setShouldTrustDevice"); + const trustDeviceSpy = jest.spyOn(deviceTrustService, "trustDevice"); + const setShouldTrustDeviceSpy = jest.spyOn(deviceTrustService, "setShouldTrustDevice"); - await deviceTrustCryptoService.trustDeviceIfRequired(mockUserId); + await deviceTrustService.trustDeviceIfRequired(mockUserId); expect(getShouldTrustDeviceSpy).toHaveBeenCalledTimes(1); expect(trustDeviceSpy).not.toHaveBeenCalled(); @@ -151,7 +153,7 @@ describe("deviceTrustCryptoService", () => { it("returns null when there is not an existing device key", async () => { await stateProvider.setUserState(DEVICE_KEY, null, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).toBeNull(); expect(secureStorageService.get).not.toHaveBeenCalled(); @@ -160,7 +162,7 @@ describe("deviceTrustCryptoService", () => { it("returns the device key when there is an existing device key", async () => { await stateProvider.setUserState(DEVICE_KEY, existingDeviceKey, mockUserId); - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); expect(deviceKey).not.toBeNull(); expect(deviceKey).toBeInstanceOf(SymmetricCryptoKey); @@ -172,17 +174,14 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("returns null when there is not an existing device key for the passed in user id", async () => { secureStorageService.get.mockResolvedValue(null); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).toBeNull(); @@ -193,7 +192,7 @@ describe("deviceTrustCryptoService", () => { secureStorageService.get.mockResolvedValue(existingDeviceKeyB64); // Act - const deviceKey = await deviceTrustCryptoService.getDeviceKey(mockUserId); + const deviceKey = await deviceTrustService.getDeviceKey(mockUserId); // Assert expect(deviceKey).not.toBeNull(); @@ -203,7 +202,7 @@ describe("deviceTrustCryptoService", () => { }); it("throws an error when no user id is passed in", async () => { - await expect(deviceTrustCryptoService.getDeviceKey(null)).rejects.toThrow( + await expect(deviceTrustService.getDeviceKey(null)).rejects.toThrow( "UserId is required. Cannot get device key.", ); }); @@ -220,7 +219,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); expect(stateProvider.mock.setUserState).toHaveBeenLastCalledWith( DEVICE_KEY, @@ -232,10 +231,7 @@ describe("deviceTrustCryptoService", () => { describe("Secure Storage supported", () => { beforeEach(() => { const supportsSecureStorage = true; - deviceTrustCryptoService = createDeviceTrustCryptoService( - mockUserId, - supportsSecureStorage, - ); + deviceTrustService = createDeviceTrustService(mockUserId, supportsSecureStorage); }); it("successfully sets the device key in secure storage", async () => { @@ -251,7 +247,7 @@ describe("deviceTrustCryptoService", () => { // Act // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - await (deviceTrustCryptoService as any).setDeviceKey(mockUserId, newDeviceKey); + await (deviceTrustService as any).setDeviceKey(mockUserId, newDeviceKey); // Assert expect(stateProvider.mock.setUserState).not.toHaveBeenCalledTimes(2); @@ -268,9 +264,9 @@ describe("deviceTrustCryptoService", () => { new Uint8Array(deviceKeyBytesLength) as CsprngArray, ) as DeviceKey; - await expect( - (deviceTrustCryptoService as any).setDeviceKey(null, newDeviceKey), - ).rejects.toThrow("UserId is required. Cannot set device key."); + await expect((deviceTrustService as any).setDeviceKey(null, newDeviceKey)).rejects.toThrow( + "UserId is required. Cannot set device key.", + ); }); }); @@ -285,7 +281,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' // This is a hacky workaround, but it allows for cleaner tests - const deviceKey = await (deviceTrustCryptoService as any).makeDeviceKey(); + const deviceKey = await (deviceTrustService as any).makeDeviceKey(); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledTimes(1); expect(keyGenSvcGenerateKeySpy).toHaveBeenCalledWith(deviceKeyBytesLength * 8); @@ -362,7 +358,7 @@ describe("deviceTrustCryptoService", () => { // TypeScript will allow calling private methods if the object is of type 'any' makeDeviceKeySpy = jest - .spyOn(deviceTrustCryptoService as any, "makeDeviceKey") + .spyOn(deviceTrustService as any, "makeDeviceKey") .mockResolvedValue(mockDeviceKey); rsaGenerateKeyPairSpy = jest @@ -398,7 +394,7 @@ describe("deviceTrustCryptoService", () => { }); it("calls the required methods with the correct arguments and returns a DeviceResponse", async () => { - const response = await deviceTrustCryptoService.trustDevice(mockUserId); + const response = await deviceTrustService.trustDevice(mockUserId); expect(makeDeviceKeySpy).toHaveBeenCalledTimes(1); expect(rsaGenerateKeyPairSpy).toHaveBeenCalledTimes(1); @@ -429,7 +425,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return null cryptoSvcGetUserKeySpy.mockResolvedValue(null); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); @@ -439,7 +435,7 @@ describe("deviceTrustCryptoService", () => { // setup the spy to return undefined cryptoSvcGetUserKeySpy.mockResolvedValue(undefined); // check if the expected error is thrown - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow( "User symmetric key not found", ); }); @@ -479,9 +475,7 @@ describe("deviceTrustCryptoService", () => { it(`throws an error if ${method} fails`, async () => { const methodSpy = spy(); methodSpy.mockRejectedValue(new Error(errorText)); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow( - errorText, - ); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(errorText); }); test.each([null, undefined])( @@ -489,14 +483,14 @@ describe("deviceTrustCryptoService", () => { async (invalidValue) => { const methodSpy = spy(); methodSpy.mockResolvedValue(invalidValue); - await expect(deviceTrustCryptoService.trustDevice(mockUserId)).rejects.toThrow(); + await expect(deviceTrustService.trustDevice(mockUserId)).rejects.toThrow(); }, ); }, ); it("throws an error when a null user id is passed in", async () => { - await expect(deviceTrustCryptoService.trustDevice(null)).rejects.toThrow( + await expect(deviceTrustService.trustDevice(null)).rejects.toThrow( "UserId is required. Cannot trust device.", ); }); @@ -530,7 +524,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + deviceTrustService.decryptUserKeyWithDeviceKey( null, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -540,7 +534,7 @@ describe("deviceTrustCryptoService", () => { }); it("returns null when device key isn't provided", async () => { - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -558,7 +552,7 @@ describe("deviceTrustCryptoService", () => { .spyOn(cryptoService, "rsaDecrypt") .mockResolvedValue(new Uint8Array(userKeyBytesLength)); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -574,9 +568,9 @@ describe("deviceTrustCryptoService", () => { const decryptToBytesSpy = jest .spyOn(encryptService, "decryptToBytes") .mockRejectedValue(new Error("Decryption error")); - const setDeviceKeySpy = jest.spyOn(deviceTrustCryptoService as any, "setDeviceKey"); + const setDeviceKeySpy = jest.spyOn(deviceTrustService as any, "setDeviceKey"); - const result = await deviceTrustCryptoService.decryptUserKeyWithDeviceKey( + const result = await deviceTrustService.decryptUserKeyWithDeviceKey( mockUserId, mockEncryptedDevicePrivateKey, mockEncryptedUserKey, @@ -606,7 +600,7 @@ describe("deviceTrustCryptoService", () => { it("throws an error when a null user id is passed in", async () => { await expect( - deviceTrustCryptoService.rotateDevicesTrust(null, fakeNewUserKey, ""), + deviceTrustService.rotateDevicesTrust(null, fakeNewUserKey, ""), ).rejects.toThrow("UserId is required. Cannot rotate device's trust."); }); @@ -615,7 +609,7 @@ describe("deviceTrustCryptoService", () => { stateProvider.activeUser.getFake(DEVICE_KEY); deviceKeyState.nextState(null); - await deviceTrustCryptoService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); + await deviceTrustService.rotateDevicesTrust(mockUserId, fakeNewUserKey, ""); expect(devicesApiService.updateTrust).not.toHaveBeenCalled(); }); @@ -691,7 +685,7 @@ describe("deviceTrustCryptoService", () => { ); }); - await deviceTrustCryptoService.rotateDevicesTrust( + await deviceTrustService.rotateDevicesTrust( mockUserId, fakeNewUserKey, "my_password_hash", @@ -713,10 +707,7 @@ describe("deviceTrustCryptoService", () => { }); // Helpers - function createDeviceTrustCryptoService( - mockUserId: UserId | null, - supportsSecureStorage: boolean, - ) { + function createDeviceTrustService(mockUserId: UserId | null, supportsSecureStorage: boolean) { accountService = mockAccountServiceWith(mockUserId); stateProvider = new FakeStateProvider(accountService); @@ -725,7 +716,7 @@ describe("deviceTrustCryptoService", () => { decryptionOptions.next({} as any); userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; - return new DeviceTrustCryptoService( + return new DeviceTrustService( keyGenerationService, cryptoFunctionService, cryptoService, @@ -737,6 +728,7 @@ describe("deviceTrustCryptoService", () => { stateProvider, secureStorageService, userDecryptionOptionsService, + logService, ); } }); diff --git a/libs/common/src/auth/services/kdf-config.service.spec.ts b/libs/common/src/auth/services/kdf-config.service.spec.ts new file mode 100644 index 0000000000..67bcf721bc --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.spec.ts @@ -0,0 +1,104 @@ +import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec"; +import { + ARGON2_ITERATIONS, + ARGON2_MEMORY, + ARGON2_PARALLELISM, + PBKDF2_ITERATIONS, +} from "../../platform/enums/kdf-type.enum"; +import { Utils } from "../../platform/misc/utils"; +import { UserId } from "../../types/guid"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +import { KdfConfigService } from "./kdf-config.service"; + +describe("KdfConfigService", () => { + let sutKdfConfigService: KdfConfigService; + + let fakeStateProvider: FakeStateProvider; + let fakeAccountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; + + beforeEach(() => { + jest.clearAllMocks(); + + fakeAccountService = mockAccountServiceWith(mockUserId); + fakeStateProvider = new FakeStateProvider(fakeAccountService); + sutKdfConfigService = new KdfConfigService(fakeStateProvider); + }); + + it("setKdfConfig(): should set the KDF config", async () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should get the KDF config", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + await expect(sutKdfConfigService.getKdfConfig()).resolves.toEqual(kdfConfig); + }); + + it("setKdfConfig(): should throw error KDF cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = null; + try { + await sutKdfConfigService.setKdfConfig(mockUserId, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("kdfConfig cannot be null")); + } + }); + + it("setKdfConfig(): should throw error userId cannot be null", async () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + try { + await sutKdfConfigService.setKdfConfig(null, kdfConfig); + } catch (e) { + expect(e).toEqual(new Error("userId cannot be null")); + } + }); + + it("getKdfConfig(): should throw error KdfConfig for active user account state is null", async () => { + try { + await sutKdfConfigService.getKdfConfig(); + } catch (e) { + expect(e).toEqual(new Error("KdfConfig for active user account state is null")); + } + }); + + it("validateKdfConfig(): should validate the PBKDF2 KDF config", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(600_000); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should validate the Argon2id KDF config", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).not.toThrow(); + }); + + it("validateKdfConfig(): should throw an error for invalid PBKDF2 iterations", () => { + const kdfConfig: PBKDF2KdfConfig = new PBKDF2KdfConfig(100); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 iterations", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(11, 64, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 memory", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 1025, 4); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, + ); + }); + + it("validateKdfConfig(): should throw an error for invalid Argon2 parallelism", () => { + const kdfConfig: Argon2KdfConfig = new Argon2KdfConfig(3, 64, 17); + expect(() => kdfConfig.validateKdfConfig()).toThrow( + `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}`, + ); + }); +}); diff --git a/libs/common/src/auth/services/kdf-config.service.ts b/libs/common/src/auth/services/kdf-config.service.ts new file mode 100644 index 0000000000..cfd2a3e1de --- /dev/null +++ b/libs/common/src/auth/services/kdf-config.service.ts @@ -0,0 +1,41 @@ +import { firstValueFrom } from "rxjs"; + +import { KdfType } from "../../platform/enums/kdf-type.enum"; +import { KDF_CONFIG_DISK, StateProvider, UserKeyDefinition } from "../../platform/state"; +import { UserId } from "../../types/guid"; +import { KdfConfigService as KdfConfigServiceAbstraction } from "../abstractions/kdf-config.service"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; + +export const KDF_CONFIG = new UserKeyDefinition(KDF_CONFIG_DISK, "kdfConfig", { + deserializer: (kdfConfig: KdfConfig) => { + if (kdfConfig == null) { + return null; + } + return kdfConfig.kdfType === KdfType.PBKDF2_SHA256 + ? PBKDF2KdfConfig.fromJSON(kdfConfig) + : Argon2KdfConfig.fromJSON(kdfConfig); + }, + clearOn: ["logout"], +}); + +export class KdfConfigService implements KdfConfigServiceAbstraction { + constructor(private stateProvider: StateProvider) {} + async setKdfConfig(userId: UserId, kdfConfig: KdfConfig) { + if (!userId) { + throw new Error("userId cannot be null"); + } + if (kdfConfig === null) { + throw new Error("kdfConfig cannot be null"); + } + await this.stateProvider.setUserState(KDF_CONFIG, kdfConfig, userId); + } + + async getKdfConfig(): Promise { + const userId = await firstValueFrom(this.stateProvider.activeUserId$); + const state = await firstValueFrom(this.stateProvider.getUser(userId, KDF_CONFIG).state$); + if (state === null) { + throw new Error("KdfConfig for active user account state is null"); + } + return state; + } +} 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 e3e5fbdbe7..0fc0267a53 100644 --- a/libs/common/src/auth/services/key-connector.service.spec.ts +++ b/libs/common/src/auth/services/key-connector.service.spec.ts @@ -215,7 +215,7 @@ describe("KeyConnectorService", () => { const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; // Act - await keyConnectorService.setMasterKeyFromUrl(url); + await keyConnectorService.setMasterKeyFromUrl(url, mockUserId); // Assert expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url); @@ -235,7 +235,7 @@ describe("KeyConnectorService", () => { try { // Act - await keyConnectorService.setMasterKeyFromUrl(url); + await keyConnectorService.setMasterKeyFromUrl(url, mockUserId); } catch { // Assert expect(logService.error).toHaveBeenCalledWith(error); diff --git a/libs/common/src/auth/services/key-connector.service.ts b/libs/common/src/auth/services/key-connector.service.ts index f8e523cce4..65d1030bd3 100644 --- a/libs/common/src/auth/services/key-connector.service.ts +++ b/libs/common/src/auth/services/key-connector.service.ts @@ -7,6 +7,7 @@ import { KeysRequest } from "../../models/request/keys.request"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { KeyGenerationService } from "../../platform/abstractions/key-generation.service"; import { LogService } from "../../platform/abstractions/log.service"; +import { KdfType } from "../../platform/enums/kdf-type.enum"; import { Utils } from "../../platform/misc/utils"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { @@ -15,12 +16,13 @@ import { StateProvider, UserKeyDefinition, } from "../../platform/state"; +import { UserId } from "../../types/guid"; import { MasterKey } from "../../types/key"; import { AccountService } from "../abstractions/account.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction"; import { TokenService } from "../abstractions/token.service"; -import { KdfConfig } from "../models/domain/kdf-config"; +import { Argon2KdfConfig, KdfConfig, PBKDF2KdfConfig } from "../models/domain/kdf-config"; import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request"; import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request"; import { IdentityTokenResponse } from "../models/response/identity-token.response"; @@ -99,12 +101,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { } // TODO: UserKey should be renamed to MasterKey and typed accordingly - async setMasterKeyFromUrl(url: string) { + async setMasterKeyFromUrl(url: string, userId: UserId) { try { const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url); const keyArr = Utils.fromB64ToArray(masterKeyResponse.key); const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey; - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setMasterKey(masterKey, userId); } catch (e) { this.handleKeyConnectorError(e); @@ -122,7 +123,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { ); } - async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) { + async convertNewSsoUserToKeyConnector( + tokenResponse: IdentityTokenResponse, + orgId: string, + userId: UserId, + ) { // TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) const { kdf, @@ -133,23 +138,24 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { userDecryptionOptions, } = tokenResponse; const password = await this.keyGenerationService.createKey(512); - const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism); + const kdfConfig: KdfConfig = + kdf === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(kdfIterations) + : new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism); const masterKey = await this.cryptoService.makeMasterKey( password.keyB64, await this.tokenService.getEmail(), - kdf, kdfConfig, ); const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64); - const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; await this.masterPasswordService.setMasterKey(masterKey, userId); const userKey = await this.cryptoService.makeUserKey(masterKey); - await this.cryptoService.setUserKey(userKey[0]); - await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString); + await this.cryptoService.setUserKey(userKey[0], userId); + await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId); - const [pubKey, privKey] = await this.cryptoService.makeKeyPair(); + const [pubKey, privKey] = await this.cryptoService.makeKeyPair(userKey[0]); try { const keyConnectorUrl = @@ -162,7 +168,6 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction { const keys = new KeysRequest(pubKey, privKey.encryptedString); const setPasswordRequest = new SetKeyConnectorKeyRequest( userKey[1].encryptedString, - kdf, kdfConfig, orgId, keys, 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 fc5060af5f..19b29f0593 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 @@ -90,6 +90,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => { const user1AccountInfo: AccountInfo = { name: "Test User 1", email: "test1@email.com", + emailVerified: true, }; activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId })); diff --git a/libs/common/src/auth/services/token.service.spec.ts b/libs/common/src/auth/services/token.service.spec.ts index d32c4d8e1c..3e92053d2f 100644 --- a/libs/common/src/auth/services/token.service.spec.ts +++ b/libs/common/src/auth/services/token.service.spec.ts @@ -23,6 +23,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe("TokenService", () => { @@ -2191,6 +2192,84 @@ describe("TokenService", () => { }); }); + describe("Security Stamp methods", () => { + const mockSecurityStamp = "securityStamp"; + + describe("setSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.setSecurityStamp(mockSecurityStamp); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot set security stamp."); + }); + + it("should set the security stamp in memory when there is an active user in global state", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + // Act + await tokenService.setSecurityStamp(mockSecurityStamp); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + + it("should set the security stamp in memory for the specified user id", async () => { + // Act + await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken); + + // Assert + expect( + singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock, + ).toHaveBeenCalledWith(mockSecurityStamp); + }); + }); + + describe("getSecurityStamp", () => { + it("should throw an error if no user id is provided and there is no active user in global state", async () => { + // Act + // note: don't await here because we want to test the error + const result = tokenService.getSecurityStamp(); + // Assert + await expect(result).rejects.toThrow("User id not found. Cannot get security stamp."); + }); + + it("should return the security stamp from memory with no user id specified (uses global active user)", async () => { + // Arrange + globalStateProvider + .getFake(ACCOUNT_ACTIVE_ACCOUNT_ID) + .stateSubject.next(userIdFromAccessToken); + + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(); + + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + + it("should return the security stamp from memory for the specified user id", async () => { + // Arrange + singleUserStateProvider + .getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY) + .stateSubject.next([userIdFromAccessToken, mockSecurityStamp]); + + // Act + const result = await tokenService.getSecurityStamp(userIdFromAccessToken); + // Assert + expect(result).toEqual(mockSecurityStamp); + }); + }); + }); + // Helpers function createTokenService(supportsSecureStorage: boolean) { return new TokenService( diff --git a/libs/common/src/auth/services/token.service.ts b/libs/common/src/auth/services/token.service.ts index c24a2c186b..56311671ad 100644 --- a/libs/common/src/auth/services/token.service.ts +++ b/libs/common/src/auth/services/token.service.ts @@ -32,6 +32,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; export enum TokenStorageLocation { @@ -251,7 +252,7 @@ export class TokenService implements TokenServiceAbstraction { if (!accessTokenKey) { // If we don't have an accessTokenKey, then that means we don't have an access token as it hasn't been set yet - // and we have to return null here to properly indicate the the user isn't logged in. + // and we have to return null here to properly indicate the user isn't logged in. return null; } @@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction { return Array.isArray(decoded.amr) && decoded.amr.includes("external"); } + async getSecurityStamp(userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot get security stamp."); + } + + const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY); + + return securityStamp; + } + + async setSecurityStamp(securityStamp: string, userId?: UserId): Promise { + userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$); + + if (!userId) { + throw new Error("User id not found. Cannot set security stamp."); + } + + await this.singleUserStateProvider + .get(userId, SECURITY_STAMP_MEMORY) + .update((_) => securityStamp); + } + private async getStateValueByUserIdAndKeyDef( userId: UserId, storageLocation: UserKeyDefinition, diff --git a/libs/common/src/auth/services/token.state.spec.ts b/libs/common/src/auth/services/token.state.spec.ts index dc00fec383..bb82410fac 100644 --- a/libs/common/src/auth/services/token.state.spec.ts +++ b/libs/common/src/auth/services/token.state.spec.ts @@ -10,6 +10,7 @@ import { EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL, REFRESH_TOKEN_DISK, REFRESH_TOKEN_MEMORY, + SECURITY_STAMP_MEMORY, } from "./token.state"; describe.each([ @@ -22,6 +23,7 @@ describe.each([ [API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"], [API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"], [API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"], + [SECURITY_STAMP_MEMORY, "securityStamp"], ])( "deserializes state key definitions", ( diff --git a/libs/common/src/auth/services/token.state.ts b/libs/common/src/auth/services/token.state.ts index 458d6846c1..57d85f2a55 100644 --- a/libs/common/src/auth/services/token.state.ts +++ b/libs/common/src/auth/services/token.state.ts @@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition( clearOn: [], // Manually handled }, ); + +export const SECURITY_STAMP_MEMORY = new UserKeyDefinition(TOKEN_MEMORY, "securityStamp", { + deserializer: (securityStamp) => securityStamp, + clearOn: ["logout"], +}); diff --git a/libs/common/src/auth/services/two-factor.service.ts b/libs/common/src/auth/services/two-factor.service.ts index cd1e5ea122..50d2556157 100644 --- a/libs/common/src/auth/services/two-factor.service.ts +++ b/libs/common/src/auth/services/two-factor.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom, map } from "rxjs"; + import { I18nService } from "../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; +import { Utils } from "../../platform/misc/utils"; +import { GlobalStateProvider, KeyDefinition, TWO_FACTOR_MEMORY } from "../../platform/state"; import { TwoFactorProviderDetails, TwoFactorService as TwoFactorServiceAbstraction, @@ -59,13 +63,36 @@ export const TwoFactorProviders: Partial, TwoFactorProviderType>( + TWO_FACTOR_MEMORY, + "providers", + { + deserializer: (obj) => obj, + }, +); + +// Memory storage as only required during authentication process +export const SELECTED_PROVIDER = new KeyDefinition( + TWO_FACTOR_MEMORY, + "selected", + { + deserializer: (obj) => obj, + }, +); + export class TwoFactorService implements TwoFactorServiceAbstraction { - private twoFactorProvidersData: Map; - private selectedTwoFactorProviderType: TwoFactorProviderType = null; + private providersState = this.globalStateProvider.get(PROVIDERS); + private selectedState = this.globalStateProvider.get(SELECTED_PROVIDER); + readonly providers$ = this.providersState.state$.pipe( + map((providers) => Utils.recordToMap(providers)), + ); + readonly selected$ = this.selectedState.state$; constructor( private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, + private globalStateProvider: GlobalStateProvider, ) {} init() { @@ -93,63 +120,60 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { this.i18nService.t("yubiKeyDesc"); } - getSupportedProviders(win: Window): TwoFactorProviderDetails[] { + async getSupportedProviders(win: Window): Promise { + const data = await firstValueFrom(this.providers$); const providers: any[] = []; - if (this.twoFactorProvidersData == null) { + if (data == null) { return providers; } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) && + data.has(TwoFactorProviderType.OrganizationDuo) && this.platformUtilsService.supportsDuo() ) { providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) { + if (data.has(TwoFactorProviderType.Authenticator)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) { + if (data.has(TwoFactorProviderType.Yubikey)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); } - if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) && - this.platformUtilsService.supportsDuo() - ) { + if (data.has(TwoFactorProviderType.Duo) && this.platformUtilsService.supportsDuo()) { providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); } if ( - this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) && + data.has(TwoFactorProviderType.WebAuthn) && this.platformUtilsService.supportsWebAuthn(win) ) { providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]); } - if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) { + if (data.has(TwoFactorProviderType.Email)) { providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); } return providers; } - getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType { - if (this.twoFactorProvidersData == null) { + async getDefaultProvider(webAuthnSupported: boolean): Promise { + const data = await firstValueFrom(this.providers$); + const selected = await firstValueFrom(this.selected$); + if (data == null) { return null; } - if ( - this.selectedTwoFactorProviderType != null && - this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType) - ) { - return this.selectedTwoFactorProviderType; + if (selected != null && data.has(selected)) { + return selected; } let providerType: TwoFactorProviderType = null; let providerPriority = -1; - this.twoFactorProvidersData.forEach((_value, type) => { + data.forEach((_value, type) => { const provider = (TwoFactorProviders as any)[type]; if (provider != null && provider.priority > providerPriority) { if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) { @@ -164,23 +188,23 @@ export class TwoFactorService implements TwoFactorServiceAbstraction { return providerType; } - setSelectedProvider(type: TwoFactorProviderType) { - this.selectedTwoFactorProviderType = type; + async setSelectedProvider(type: TwoFactorProviderType): Promise { + await this.selectedState.update(() => type); } - clearSelectedProvider() { - this.selectedTwoFactorProviderType = null; + async clearSelectedProvider(): Promise { + await this.selectedState.update(() => null); } - setProviders(response: IdentityTwoFactorResponse) { - this.twoFactorProvidersData = response.twoFactorProviders2; + async setProviders(response: IdentityTwoFactorResponse): Promise { + await this.providersState.update(() => response.twoFactorProviders2); } - clearProviders() { - this.twoFactorProvidersData = null; + async clearProviders(): Promise { + await this.providersState.update(() => null); } - getProviders() { - return this.twoFactorProvidersData; + getProviders(): Promise> { + return firstValueFrom(this.providers$); } } 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 5a443b784d..94adad8bc7 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 @@ -13,6 +13,7 @@ import { KeySuffixOptions } from "../../../platform/enums/key-suffix-options.enu import { UserId } from "../../../types/guid"; import { UserKey } from "../../../types/key"; import { AccountService } from "../../abstractions/account.service"; +import { KdfConfigService } from "../../abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction"; @@ -47,6 +48,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private platformUtilsService: PlatformUtilsService, + private kdfConfigService: KdfConfigService, ) {} async getAvailableVerificationOptions( @@ -118,8 +120,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } request.masterPasswordHash = alreadyHashed @@ -176,8 +177,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti masterKey = await this.cryptoService.makeMasterKey( verification.secret, await this.stateService.getEmail(), - await this.stateService.getKdfType(), - await this.stateService.getKdfConfig(), + await this.kdfConfigService.getKdfConfig(), ); } const passwordValid = await this.cryptoService.compareAndUpdateKeyHash( diff --git a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts index 1311976c4b..063b3c370b 100644 --- a/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts +++ b/libs/common/src/billing/abstractions/billilng-api.service.abstraction.ts @@ -1,6 +1,11 @@ import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; +import { OrganizationBillingMetadataResponse } from "../../billing/models/response/organization-billing-metadata.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; -import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; +import { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export abstract class BillingApiServiceAbstraction { @@ -9,11 +14,22 @@ export abstract class BillingApiServiceAbstraction { request: SubscriptionCancellationRequest, ) => Promise; cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise; + createClientOrganization: ( + providerId: string, + request: CreateClientOrganizationRequest, + ) => Promise; getBillingStatus: (id: string) => Promise; - getProviderClientSubscriptions: (providerId: string) => Promise; - putProviderClientSubscriptions: ( + getOrganizationBillingMetadata: ( + organizationId: string, + ) => Promise; + getOrganizationSubscription: ( + organizationId: string, + ) => Promise; + getPlans: () => Promise>; + getProviderSubscription: (providerId: string) => Promise; + updateClientOrganization: ( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ) => Promise; } diff --git a/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts b/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts new file mode 100644 index 0000000000..0f65dee408 --- /dev/null +++ b/libs/common/src/billing/abstractions/provider-billing.service.abstraction.ts @@ -0,0 +1,25 @@ +import { map, Observable, OperatorFunction, switchMap } from "rxjs"; + +import { ProviderStatusType } from "@bitwarden/common/admin-console/enums"; +import { Provider } from "@bitwarden/common/admin-console/models/domain/provider"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; + +type MaybeProvider = Provider | undefined; + +export const canAccessBilling = ( + configService: ConfigService, +): OperatorFunction => + switchMap>((provider) => + configService + .getFeatureFlag$(FeatureFlag.EnableConsolidatedBilling) + .pipe( + map((consolidatedBillingEnabled) => + provider + ? provider.isProviderAdmin && + provider.providerStatus === ProviderStatusType.Billable && + consolidatedBillingEnabled + : false, + ), + ), + ); diff --git a/libs/common/src/billing/models/request/create-client-organization.request.ts b/libs/common/src/billing/models/request/create-client-organization.request.ts new file mode 100644 index 0000000000..2eac23531a --- /dev/null +++ b/libs/common/src/billing/models/request/create-client-organization.request.ts @@ -0,0 +1,12 @@ +import { OrganizationKeysRequest } from "../../../admin-console/models/request/organization-keys.request"; +import { PlanType } from "../../../billing/enums"; + +export class CreateClientOrganizationRequest { + name: string; + ownerEmail: string; + planType: PlanType; + seats: number; + key: string; + keyPair: OrganizationKeysRequest; + collectionName: string; +} diff --git a/libs/common/src/billing/models/request/provider-subscription-update.request.ts b/libs/common/src/billing/models/request/provider-subscription-update.request.ts deleted file mode 100644 index f2bf4c7e97..0000000000 --- a/libs/common/src/billing/models/request/provider-subscription-update.request.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class ProviderSubscriptionUpdateRequest { - assignedSeats: number; -} diff --git a/libs/common/src/billing/models/request/update-client-organization.request.ts b/libs/common/src/billing/models/request/update-client-organization.request.ts new file mode 100644 index 0000000000..16dbe1e17d --- /dev/null +++ b/libs/common/src/billing/models/request/update-client-organization.request.ts @@ -0,0 +1,3 @@ +export class UpdateClientOrganizationRequest { + assignedSeats: number; +} 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 new file mode 100644 index 0000000000..33d7907fa8 --- /dev/null +++ b/libs/common/src/billing/models/response/organization-billing-metadata.response.ts @@ -0,0 +1,10 @@ +import { BaseResponse } from "../../../models/response/base.response"; + +export class OrganizationBillingMetadataResponse extends BaseResponse { + isOnSecretsManagerStandalone: boolean; + + constructor(response: any) { + super(response); + this.isOnSecretsManagerStandalone = this.getResponseProperty("IsOnSecretsManagerStandalone"); + } +} diff --git a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts index c1f5640207..7b49688294 100644 --- a/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts +++ b/libs/common/src/billing/models/view/self-hosted-organization-subscription.view.ts @@ -58,4 +58,16 @@ export class SelfHostedOrganizationSubscriptionView implements View { get isExpiredAndOutsideGracePeriod() { return this.hasExpiration && this.expirationWithGracePeriod < new Date(); } + + /** + * In the case of a trial, where there is no grace period, the expirationWithGracePeriod and expirationWithoutGracePeriod will + * be exactly the same. This can be used to hide the grace period note. + */ + get isInTrial() { + return ( + this.expirationWithGracePeriod && + this.expirationWithoutGracePeriod && + this.expirationWithGracePeriod.getTime() === this.expirationWithoutGracePeriod.getTime() + ); + } } diff --git a/libs/common/src/billing/services/billing-api.service.ts b/libs/common/src/billing/services/billing-api.service.ts index 48866ab90d..d21c1c9046 100644 --- a/libs/common/src/billing/services/billing-api.service.ts +++ b/libs/common/src/billing/services/billing-api.service.ts @@ -1,8 +1,14 @@ +import { OrganizationBillingMetadataResponse } from "@bitwarden/common/billing/models/response/organization-billing-metadata.response"; + import { ApiService } from "../../abstractions/api.service"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; -import { ProviderSubscriptionUpdateRequest } from "../models/request/provider-subscription-update.request"; +import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response"; +import { PlanResponse } from "../../billing/models/response/plan.response"; +import { ListResponse } from "../../models/response/list.response"; +import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; +import { UpdateClientOrganizationRequest } from "../models/request/update-client-organization.request"; import { ProviderSubscriptionResponse } from "../models/response/provider-subscription-response"; export class BillingApiService implements BillingApiServiceAbstraction { @@ -25,6 +31,19 @@ export class BillingApiService implements BillingApiServiceAbstraction { return this.apiService.send("POST", "/accounts/cancel", request, true, false); } + createClientOrganization( + providerId: string, + request: CreateClientOrganizationRequest, + ): Promise { + return this.apiService.send( + "POST", + "/providers/" + providerId + "/clients", + request, + true, + false, + ); + } + async getBillingStatus(id: string): Promise { const r = await this.apiService.send( "GET", @@ -33,11 +52,42 @@ export class BillingApiService implements BillingApiServiceAbstraction { true, true, ); - return new OrganizationBillingStatusResponse(r); } - async getProviderClientSubscriptions(providerId: string): Promise { + async getOrganizationBillingMetadata( + organizationId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/billing/metadata", + null, + true, + true, + ); + + return new OrganizationBillingMetadataResponse(r); + } + + async getOrganizationSubscription( + organizationId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/subscription", + null, + true, + true, + ); + return new OrganizationSubscriptionResponse(r); + } + + async getPlans(): Promise> { + const r = await this.apiService.send("GET", "/plans", null, false, true); + return new ListResponse(r, PlanResponse); + } + + async getProviderSubscription(providerId: string): Promise { const r = await this.apiService.send( "GET", "/providers/" + providerId + "/billing/subscription", @@ -48,14 +98,14 @@ export class BillingApiService implements BillingApiServiceAbstraction { return new ProviderSubscriptionResponse(r); } - async putProviderClientSubscriptions( + async updateClientOrganization( providerId: string, organizationId: string, - request: ProviderSubscriptionUpdateRequest, + request: UpdateClientOrganizationRequest, ): Promise { return await this.apiService.send( "PUT", - "/providers/" + providerId + "/organizations/" + organizationId, + "/providers/" + providerId + "/clients/" + organizationId, request, true, false, diff --git a/libs/common/src/billing/services/organization-billing.service.ts b/libs/common/src/billing/services/organization-billing.service.ts index f2df30e4e0..6b326472c9 100644 --- a/libs/common/src/billing/services/organization-billing.service.ts +++ b/libs/common/src/billing/services/organization-billing.service.ts @@ -1,3 +1,4 @@ +import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction as OrganizationApiService } from "../../admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationCreateRequest } from "../../admin-console/models/request/organization-create.request"; import { OrganizationKeysRequest } from "../../admin-console/models/request/organization-keys.request"; @@ -7,6 +8,7 @@ import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { I18nService } from "../../platform/abstractions/i18n.service"; import { EncString } from "../../platform/models/domain/enc-string"; import { OrgKey } from "../../types/key"; +import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; import { OrganizationBillingServiceAbstraction, OrganizationInformation, @@ -25,10 +27,12 @@ interface OrganizationKeys { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { constructor( + private apiService: ApiService, private cryptoService: CryptoService, private encryptService: EncryptService, private i18nService: I18nService, private organizationApiService: OrganizationApiService, + private syncService: SyncService, ) {} async purchaseSubscription(subscription: SubscriptionInformation): Promise { @@ -44,7 +48,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPaymentInformation(request, subscription.payment); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } async startFree(subscription: SubscriptionInformation): Promise { @@ -58,7 +68,13 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs this.setPlanInformation(request, subscription.plan); - return await this.organizationApiService.create(request); + const response = await this.organizationApiService.create(request); + + await this.apiService.refreshIdentityToken(); + + await this.syncService.fullSync(true); + + return response; } private async makeOrganizationKeys(): Promise { diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 636e9bc4ce..221b251f3c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -1,3 +1,8 @@ +/** + * Feature flags. + * + * Flags MUST be short lived and SHALL be removed once enabled. + */ export enum FeatureFlag { BrowserFilelessImport = "browser-fileless-import", ItemShare = "item-share", @@ -11,7 +16,38 @@ export enum FeatureFlag { AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", UnassignedItemsBanner = "unassigned-items-banner", EnableDeleteProvider = "AC-1218-delete-provider", + ExtensionRefresh = "extension-refresh", + RestrictProviderAccess = "restrict-provider-access", } -// Replace this with a type safe lookup of the feature flag values in PM-2282 -export type FeatureFlagValue = number | string | boolean; +export type AllowedFeatureFlagTypes = boolean | number | string; + +// Helper to ensure the value is treated as a boolean. +const FALSE = false as boolean; + +/** + * Default value for feature flags. + * + * DO NOT enable previously disabled flags, REMOVE them instead. + * We support true as a value as we prefer flags to "enable" not "disable". + */ +export const DefaultFeatureFlagValue = { + [FeatureFlag.BrowserFilelessImport]: FALSE, + [FeatureFlag.ItemShare]: FALSE, + [FeatureFlag.FlexibleCollectionsV1]: FALSE, + [FeatureFlag.VaultOnboarding]: FALSE, + [FeatureFlag.GeneratorToolsModernization]: FALSE, + [FeatureFlag.KeyRotationImprovements]: FALSE, + [FeatureFlag.FlexibleCollectionsMigration]: FALSE, + [FeatureFlag.ShowPaymentMethodWarningBanners]: FALSE, + [FeatureFlag.EnableConsolidatedBilling]: FALSE, + [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, + [FeatureFlag.UnassignedItemsBanner]: FALSE, + [FeatureFlag.EnableDeleteProvider]: FALSE, + [FeatureFlag.ExtensionRefresh]: FALSE, + [FeatureFlag.RestrictProviderAccess]: FALSE, +} satisfies Record; + +export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue; + +export type FeatureFlagValueType = DefaultFeatureFlagValueType[Flag]; diff --git a/libs/common/src/enums/index.ts b/libs/common/src/enums/index.ts index 378af213e6..9ca806899a 100644 --- a/libs/common/src/enums/index.ts +++ b/libs/common/src/enums/index.ts @@ -3,6 +3,7 @@ export * from "./device-type.enum"; export * from "./event-system-user.enum"; export * from "./event-type.enum"; export * from "./http-status-code.enum"; +export * from "./integration-type.enum"; export * from "./native-messaging-version.enum"; export * from "./notification-type.enum"; export * from "./product-type.enum"; diff --git a/libs/common/src/enums/integration-type.enum.ts b/libs/common/src/enums/integration-type.enum.ts new file mode 100644 index 0000000000..acb9510697 --- /dev/null +++ b/libs/common/src/enums/integration-type.enum.ts @@ -0,0 +1,4 @@ +export enum IntegrationType { + Integration = "integration", + SDK = "sdk", +} diff --git a/libs/common/src/models/export/card.export.ts b/libs/common/src/models/export/card.export.ts index 55bb3a7be1..151b447e86 100644 --- a/libs/common/src/models/export/card.export.ts +++ b/libs/common/src/models/export/card.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Card as CardDomain } from "../../vault/models/domain/card"; import { CardView } from "../../vault/models/view/card.view"; +import { safeGetString } from "./utils"; + export class CardExport { static template(): CardExport { const req = new CardExport(); @@ -46,20 +48,11 @@ export class CardExport { return; } - if (o instanceof CardView) { - this.cardholderName = o.cardholderName; - this.brand = o.brand; - this.number = o.number; - this.expMonth = o.expMonth; - this.expYear = o.expYear; - this.code = o.code; - } else { - this.cardholderName = o.cardholderName?.encryptedString; - this.brand = o.brand?.encryptedString; - this.number = o.number?.encryptedString; - this.expMonth = o.expMonth?.encryptedString; - this.expYear = o.expYear?.encryptedString; - this.code = o.code?.encryptedString; - } + this.cardholderName = safeGetString(o.cardholderName); + this.brand = safeGetString(o.brand); + this.number = safeGetString(o.number); + this.expMonth = safeGetString(o.expMonth); + this.expYear = safeGetString(o.expYear); + this.code = safeGetString(o.code); } } diff --git a/libs/common/src/models/export/cipher.export.ts b/libs/common/src/models/export/cipher.export.ts index 3ae6c9757d..64583f7fce 100644 --- a/libs/common/src/models/export/cipher.export.ts +++ b/libs/common/src/models/export/cipher.export.ts @@ -10,6 +10,7 @@ import { IdentityExport } from "./identity.export"; import { LoginExport } from "./login.export"; import { PasswordHistoryExport } from "./password-history.export"; import { SecureNoteExport } from "./secure-note.export"; +import { safeGetString } from "./utils"; export class CipherExport { static template(): CipherExport { @@ -145,23 +146,16 @@ export class CipherExport { this.type = o.type; this.reprompt = o.reprompt; - if (o instanceof CipherView) { - this.name = o.name; - this.notes = o.notes; - } else { - this.name = o.name?.encryptedString; - this.notes = o.notes?.encryptedString; + this.name = safeGetString(o.name); + this.notes = safeGetString(o.notes); + if ("key" in o) { this.key = o.key?.encryptedString; } this.favorite = o.favorite; if (o.fields != null) { - if (o instanceof CipherView) { - this.fields = o.fields.map((f) => new FieldExport(f)); - } else { - this.fields = o.fields.map((f) => new FieldExport(f)); - } + this.fields = o.fields.map((f) => new FieldExport(f)); } switch (o.type) { @@ -180,11 +174,7 @@ export class CipherExport { } if (o.passwordHistory != null) { - if (o instanceof CipherView) { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } else { - this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); - } + this.passwordHistory = o.passwordHistory.map((ph) => new PasswordHistoryExport(ph)); } this.creationDate = o.creationDate; diff --git a/libs/common/src/models/export/collection.export.ts b/libs/common/src/models/export/collection.export.ts index 48251d581f..c94d5bc0ca 100644 --- a/libs/common/src/models/export/collection.export.ts +++ b/libs/common/src/models/export/collection.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Collection as CollectionDomain } from "../../vault/models/domain/collection"; import { CollectionView } from "../../vault/models/view/collection.view"; +import { safeGetString } from "./utils"; + export class CollectionExport { static template(): CollectionExport { const req = new CollectionExport(); @@ -36,11 +38,7 @@ export class CollectionExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: CollectionView | CollectionDomain) { this.organizationId = o.organizationId; - if (o instanceof CollectionView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); this.externalId = o.externalId; } } diff --git a/libs/common/src/models/export/fido2-credential.export.ts b/libs/common/src/models/export/fido2-credential.export.ts index d41b7d67c9..4c60d148db 100644 --- a/libs/common/src/models/export/fido2-credential.export.ts +++ b/libs/common/src/models/export/fido2-credential.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Fido2Credential } from "../../vault/models/domain/fido2-credential"; import { Fido2CredentialView } from "../../vault/models/view/fido2-credential.view"; +import { safeGetString } from "./utils"; + /** * Represents format of Fido2 Credentials in JSON exports. */ @@ -99,33 +101,18 @@ export class Fido2CredentialExport { return; } - if (o instanceof Fido2CredentialView) { - this.credentialId = o.credentialId; - this.keyType = o.keyType; - this.keyAlgorithm = o.keyAlgorithm; - this.keyCurve = o.keyCurve; - this.keyValue = o.keyValue; - this.rpId = o.rpId; - this.userHandle = o.userHandle; - this.userName = o.userName; - this.counter = String(o.counter); - this.rpName = o.rpName; - this.userDisplayName = o.userDisplayName; - this.discoverable = String(o.discoverable); - } else { - this.credentialId = o.credentialId?.encryptedString; - this.keyType = o.keyType?.encryptedString; - this.keyAlgorithm = o.keyAlgorithm?.encryptedString; - this.keyCurve = o.keyCurve?.encryptedString; - this.keyValue = o.keyValue?.encryptedString; - this.rpId = o.rpId?.encryptedString; - this.userHandle = o.userHandle?.encryptedString; - this.userName = o.userName?.encryptedString; - this.counter = o.counter?.encryptedString; - this.rpName = o.rpName?.encryptedString; - this.userDisplayName = o.userDisplayName?.encryptedString; - this.discoverable = o.discoverable?.encryptedString; - } + this.credentialId = safeGetString(o.credentialId); + this.keyType = safeGetString(o.keyType); + this.keyAlgorithm = safeGetString(o.keyAlgorithm); + this.keyCurve = safeGetString(o.keyCurve); + this.keyValue = safeGetString(o.keyValue); + this.rpId = safeGetString(o.rpId); + this.userHandle = safeGetString(o.userHandle); + this.userName = safeGetString(o.userName); + this.counter = safeGetString(String(o.counter)); + this.rpName = safeGetString(o.rpName); + this.userDisplayName = safeGetString(o.userDisplayName); + this.discoverable = safeGetString(String(o.discoverable)); this.creationDate = o.creationDate; } } diff --git a/libs/common/src/models/export/field.export.ts b/libs/common/src/models/export/field.export.ts index 098249312c..5ba341af61 100644 --- a/libs/common/src/models/export/field.export.ts +++ b/libs/common/src/models/export/field.export.ts @@ -3,6 +3,8 @@ import { FieldType, LinkedIdType } from "../../vault/enums"; import { Field as FieldDomain } from "../../vault/models/domain/field"; import { FieldView } from "../../vault/models/view/field.view"; +import { safeGetString } from "./utils"; + export class FieldExport { static template(): FieldExport { const req = new FieldExport(); @@ -38,13 +40,8 @@ export class FieldExport { return; } - if (o instanceof FieldView) { - this.name = o.name; - this.value = o.value; - } else { - this.name = o.name?.encryptedString; - this.value = o.value?.encryptedString; - } + this.name = safeGetString(o.name); + this.value = safeGetString(o.value); this.type = o.type; this.linkedId = o.linkedId; } diff --git a/libs/common/src/models/export/folder.export.ts b/libs/common/src/models/export/folder.export.ts index 4015034ebe..6a2a63a77d 100644 --- a/libs/common/src/models/export/folder.export.ts +++ b/libs/common/src/models/export/folder.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Folder as FolderDomain } from "../../vault/models/domain/folder"; import { FolderView } from "../../vault/models/view/folder.view"; +import { safeGetString } from "./utils"; + export class FolderExport { static template(): FolderExport { const req = new FolderExport(); @@ -23,10 +25,6 @@ export class FolderExport { // Use build method instead of ctor so that we can control order of JSON stringify for pretty print build(o: FolderView | FolderDomain) { - if (o instanceof FolderView) { - this.name = o.name; - } else { - this.name = o.name?.encryptedString; - } + this.name = safeGetString(o.name); } } diff --git a/libs/common/src/models/export/identity.export.ts b/libs/common/src/models/export/identity.export.ts index 2eb9c8364f..6722333d79 100644 --- a/libs/common/src/models/export/identity.export.ts +++ b/libs/common/src/models/export/identity.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Identity as IdentityDomain } from "../../vault/models/domain/identity"; import { IdentityView } from "../../vault/models/view/identity.view"; +import { safeGetString } from "./utils"; + export class IdentityExport { static template(): IdentityExport { const req = new IdentityExport(); @@ -94,44 +96,23 @@ export class IdentityExport { return; } - if (o instanceof IdentityView) { - this.title = o.title; - this.firstName = o.firstName; - this.middleName = o.middleName; - this.lastName = o.lastName; - this.address1 = o.address1; - this.address2 = o.address2; - this.address3 = o.address3; - this.city = o.city; - this.state = o.state; - this.postalCode = o.postalCode; - this.country = o.country; - this.company = o.company; - this.email = o.email; - this.phone = o.phone; - this.ssn = o.ssn; - this.username = o.username; - this.passportNumber = o.passportNumber; - this.licenseNumber = o.licenseNumber; - } else { - this.title = o.title?.encryptedString; - this.firstName = o.firstName?.encryptedString; - this.middleName = o.middleName?.encryptedString; - this.lastName = o.lastName?.encryptedString; - this.address1 = o.address1?.encryptedString; - this.address2 = o.address2?.encryptedString; - this.address3 = o.address3?.encryptedString; - this.city = o.city?.encryptedString; - this.state = o.state?.encryptedString; - this.postalCode = o.postalCode?.encryptedString; - this.country = o.country?.encryptedString; - this.company = o.company?.encryptedString; - this.email = o.email?.encryptedString; - this.phone = o.phone?.encryptedString; - this.ssn = o.ssn?.encryptedString; - this.username = o.username?.encryptedString; - this.passportNumber = o.passportNumber?.encryptedString; - this.licenseNumber = o.licenseNumber?.encryptedString; - } + this.title = safeGetString(o.title); + this.firstName = safeGetString(o.firstName); + this.middleName = safeGetString(o.middleName); + this.lastName = safeGetString(o.lastName); + this.address1 = safeGetString(o.address1); + this.address2 = safeGetString(o.address2); + this.address3 = safeGetString(o.address3); + this.city = safeGetString(o.city); + this.state = safeGetString(o.state); + this.postalCode = safeGetString(o.postalCode); + this.country = safeGetString(o.country); + this.company = safeGetString(o.company); + this.email = safeGetString(o.email); + this.phone = safeGetString(o.phone); + this.ssn = safeGetString(o.ssn); + this.username = safeGetString(o.username); + this.passportNumber = safeGetString(o.passportNumber); + this.licenseNumber = safeGetString(o.licenseNumber); } } diff --git a/libs/common/src/models/export/login-uri.export.ts b/libs/common/src/models/export/login-uri.export.ts index 83a7d25eff..a053446061 100644 --- a/libs/common/src/models/export/login-uri.export.ts +++ b/libs/common/src/models/export/login-uri.export.ts @@ -3,6 +3,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUriView } from "../../vault/models/view/login-uri.view"; +import { safeGetString } from "./utils"; + export class LoginUriExport { static template(): LoginUriExport { const req = new LoginUriExport(); @@ -33,10 +35,8 @@ export class LoginUriExport { return; } - if (o instanceof LoginUriView) { - this.uri = o.uri; - } else { - this.uri = o.uri?.encryptedString; + this.uri = safeGetString(o.uri); + if ("uriChecksum" in o) { this.uriChecksum = o.uriChecksum?.encryptedString; } this.match = o.match; diff --git a/libs/common/src/models/export/login.export.ts b/libs/common/src/models/export/login.export.ts index a5d9348c2c..6982d386c3 100644 --- a/libs/common/src/models/export/login.export.ts +++ b/libs/common/src/models/export/login.export.ts @@ -4,6 +4,7 @@ import { LoginView } from "../../vault/models/view/login.view"; import { Fido2CredentialExport } from "./fido2-credential.export"; import { LoginUriExport } from "./login-uri.export"; +import { safeGetString } from "./utils"; export class LoginExport { static template(): LoginExport { @@ -53,25 +54,15 @@ export class LoginExport { } if (o.uris != null) { - if (o instanceof LoginView) { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } else { - this.uris = o.uris.map((u) => new LoginUriExport(u)); - } + this.uris = o.uris.map((u) => new LoginUriExport(u)); } if (o.fido2Credentials != null) { this.fido2Credentials = o.fido2Credentials.map((key) => new Fido2CredentialExport(key)); } - if (o instanceof LoginView) { - this.username = o.username; - this.password = o.password; - this.totp = o.totp; - } else { - this.username = o.username?.encryptedString; - this.password = o.password?.encryptedString; - this.totp = o.totp?.encryptedString; - } + this.username = safeGetString(o.username); + this.password = safeGetString(o.password); + this.totp = safeGetString(o.totp); } } diff --git a/libs/common/src/models/export/password-history.export.ts b/libs/common/src/models/export/password-history.export.ts index 0bdbc6697a..fff22de8de 100644 --- a/libs/common/src/models/export/password-history.export.ts +++ b/libs/common/src/models/export/password-history.export.ts @@ -2,6 +2,8 @@ import { EncString } from "../../platform/models/domain/enc-string"; import { Password } from "../../vault/models/domain/password"; import { PasswordHistoryView } from "../../vault/models/view/password-history.view"; +import { safeGetString } from "./utils"; + export class PasswordHistoryExport { static template(): PasswordHistoryExport { const req = new PasswordHistoryExport(); @@ -30,11 +32,7 @@ export class PasswordHistoryExport { return; } - if (o instanceof PasswordHistoryView) { - this.password = o.password; - } else { - this.password = o.password?.encryptedString; - } + this.password = safeGetString(o.password); this.lastUsedDate = o.lastUsedDate; } } diff --git a/libs/common/src/models/export/utils.ts b/libs/common/src/models/export/utils.ts new file mode 100644 index 0000000000..630b489850 --- /dev/null +++ b/libs/common/src/models/export/utils.ts @@ -0,0 +1,12 @@ +import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +export function safeGetString(value: string | EncString) { + if (value == null) { + return null; + } + + if (typeof value == "string") { + return value; + } + return value?.encryptedString; +} diff --git a/libs/common/src/models/response/profile.response.ts b/libs/common/src/models/response/profile.response.ts index fbaa4f84ef..3d5fde9ac4 100644 --- a/libs/common/src/models/response/profile.response.ts +++ b/libs/common/src/models/response/profile.response.ts @@ -1,11 +1,12 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; +import { UserId } from "../../types/guid"; import { BaseResponse } from "./base.response"; export class ProfileResponse extends BaseResponse { - id: string; + id: UserId; name: string; email: string; emailVerified: boolean; diff --git a/libs/common/src/platform/abstractions/config/config.service.ts b/libs/common/src/platform/abstractions/config/config.service.ts index 9eca5891ac..6985430acc 100644 --- a/libs/common/src/platform/abstractions/config/config.service.ts +++ b/libs/common/src/platform/abstractions/config/config.service.ts @@ -1,7 +1,7 @@ import { Observable } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag } from "../../../enums/feature-flag.enum"; +import { FeatureFlag, FeatureFlagValueType } from "../../../enums/feature-flag.enum"; import { Region } from "../environment.service"; import { ServerConfig } from "./server-config"; @@ -14,23 +14,15 @@ export abstract class ConfigService { /** * Retrieves the value of a feature flag for the currently active user * @param key The feature flag to retrieve - * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable * @returns An observable that emits the value of the feature flag, updates as the server config changes */ - getFeatureFlag$: ( - key: FeatureFlag, - defaultValue?: T, - ) => Observable; + getFeatureFlag$: (key: Flag) => Observable>; /** * Retrieves the value of a feature flag for the currently active user * @param key The feature flag to retrieve - * @param defaultValue The default value to return if the feature flag is not set or the server's config is irretrievable * @returns The value of the feature flag */ - getFeatureFlag: ( - key: FeatureFlag, - defaultValue?: T, - ) => Promise; + getFeatureFlag: (key: Flag) => Promise>; /** * Verifies whether the server version meets the minimum required version * @param minimumRequiredServerVersion The minimum version required diff --git a/libs/common/src/platform/abstractions/config/server-config.ts b/libs/common/src/platform/abstractions/config/server-config.ts index 287e359f18..bb18605964 100644 --- a/libs/common/src/platform/abstractions/config/server-config.ts +++ b/libs/common/src/platform/abstractions/config/server-config.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { ServerConfigData, ThirdPartyServerConfigData, @@ -14,7 +15,7 @@ export class ServerConfig { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: Date; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigData: ServerConfigData) { this.version = serverConfigData.version; diff --git a/libs/common/src/platform/abstractions/crypto.service.ts b/libs/common/src/platform/abstractions/crypto.service.ts index 6609a1014e..43f47b0a3d 100644 --- a/libs/common/src/platform/abstractions/crypto.service.ts +++ b/libs/common/src/platform/abstractions/crypto.service.ts @@ -6,7 +6,7 @@ import { ProfileProviderResponse } from "../../admin-console/models/response/pro import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; -import { KeySuffixOptions, KdfType, HashPurpose } from "../enums"; +import { KeySuffixOptions, HashPurpose } from "../enums"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncString } from "../models/domain/enc-string"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -54,13 +54,23 @@ export abstract class CryptoService { * for encryption of data instead of the user key. */ abstract isLegacyUser(masterKey?: MasterKey, userId?: string): Promise; + + /** + * Use for encryption/decryption of data in order to support legacy + * encryption models. It will return the user key if available, + * if not it will return the master key. + * + * @deprecated Please provide the userId of the user you want the user key for. + */ + abstract getUserKeyWithLegacySupport(): Promise; + /** * Use for encryption/decryption of data in order to support legacy * encryption models. It will return the user key if available, * if not it will return the master key. * @param userId The desired user */ - abstract getUserKeyWithLegacySupport(userId?: string): Promise; + abstract getUserKeyWithLegacySupport(userId: UserId): Promise; /** * Retrieves the user key from storage * @param keySuffix The desired version of the user's key to retrieve @@ -114,16 +124,10 @@ export abstract class CryptoService { * Generates a master key from the provided password * @param password The user's master password * @param email The user's email - * @param kdf The user's selected key derivation function to use * @param KdfConfig The user's key derivation function configuration * @returns A master key derived from the provided password */ - abstract makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise; + abstract makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise; /** * Encrypts the existing (or provided) user key with the * provided master key @@ -175,10 +179,12 @@ export abstract class CryptoService { * organization keys currently in memory * @param orgs The organizations to set keys for * @param providerOrgs The provider organizations to set keys for + * @param userId The user id of the user to set the org keys for */ abstract setOrgKeys( orgs: ProfileOrganizationResponse[], providerOrgs: ProfileProviderOrganizationResponse[], + userId: UserId, ): Promise; abstract activeUserOrgKeys$: Observable>; /** @@ -206,7 +212,13 @@ export abstract class CryptoService { * @param providers The providers to set keys for */ abstract activeUserProviderKeys$: Observable>; - abstract setProviderKeys(orgs: ProfileProviderResponse[]): Promise; + + /** + * Stores the provider keys for a given user. + * @param orgs The provider orgs for which to save the keys from. + * @param userId The user id of the user for which to store the keys for. + */ + abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise; /** * @param providerId The desired provider * @returns The provider's symmetric key @@ -229,12 +241,12 @@ export abstract class CryptoService { */ abstract makeOrgKey(): Promise<[EncString, T]>; /** - * Sets the the user's encrypted private key in storage and + * Sets the user's encrypted private key in storage and * clears the decrypted private key from memory * Note: does not clear the private key if null is provided * @param encPrivateKey An encrypted private key */ - abstract setPrivateKey(encPrivateKey: string): Promise; + abstract setPrivateKey(encPrivateKey: string, userId: UserId): Promise; /** * Returns the private key from memory. If not available, decrypts it * from storage and stores it in memory @@ -253,21 +265,16 @@ export abstract class CryptoService { * @param key A key to encrypt the private key with. If not provided, * defaults to the user key * @returns A new keypair: [publicKey in Base64, encrypted privateKey] + * @throws If the provided key is a null-ish value. */ - abstract makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]>; + abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>; /** * @param pin The user's pin * @param salt The user's salt - * @param kdf The user's kdf * @param kdfConfig The user's kdf config * @returns A key derived from the user's pin */ - abstract makePinKey( - pin: string, - salt: string, - kdf: KdfType, - kdfConfig: KdfConfig, - ): Promise; + abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise; /** * Clears the user's pin keys from storage * Note: This will remove the stored pin and as a result, @@ -279,7 +286,6 @@ export abstract class CryptoService { * Decrypts the user key with their pin * @param pin The user's PIN * @param salt The user's salt - * @param kdf The user's KDF * @param kdfConfig The user's KDF config * @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided * it will be retrieved from storage @@ -288,7 +294,6 @@ export abstract class CryptoService { abstract decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; @@ -298,7 +303,6 @@ export abstract class CryptoService { * @param masterPasswordOnRestart True if Master Password on Restart is enabled * @param pin User's PIN * @param email User's email - * @param kdf User's KdfType * @param kdfConfig User's KdfConfig * @param oldPinKey The old Pin key from state (retrieved from different * places depending on if Master Password on Restart was enabled) @@ -308,14 +312,9 @@ export abstract class CryptoService { masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise; - /** - * Replaces old master auto keys with new user auto keys - */ - abstract migrateAutoKeyIfNeeded(userId?: string): Promise; /** * @param keyMaterial The key material to derive the send key from * @returns A new send key @@ -331,15 +330,17 @@ export abstract class CryptoService { * @param data The data to encrypt * @param publicKey The public key to use for encryption, if not provided, the user's public key will be used * @returns The encrypted data + * @throws If the given publicKey is a null-ish value. */ - abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise; + abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise; /** * Decrypts a value using RSA. * @param encValue The encrypted value to decrypt - * @param privateKeyValue The private key to use for decryption + * @param privateKey The private key to use for decryption * @returns The decrypted value + * @throws If the given privateKey is a null-ish value. */ - abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise; + abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise; abstract randomNumber(min: number, max: number): Promise; /** * Generates a new cipher key @@ -358,21 +359,12 @@ export abstract class CryptoService { privateKey: EncString; }>; - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - abstract validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void; - /** * @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead. */ abstract decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, protectedKeyCs?: EncString, ): Promise; diff --git a/libs/common/src/platform/abstractions/key-generation.service.ts b/libs/common/src/platform/abstractions/key-generation.service.ts index 223eb75038..3a6971ba5d 100644 --- a/libs/common/src/platform/abstractions/key-generation.service.ts +++ b/libs/common/src/platform/abstractions/key-generation.service.ts @@ -1,6 +1,5 @@ import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; -import { KdfType } from "../enums"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; export abstract class KeyGenerationService { @@ -46,14 +45,12 @@ export abstract class KeyGenerationService { * Derives a 32 byte key from a password using a key derivation function. * @param password Password to derive the key from. * @param salt Salt for the key derivation function. - * @param kdf Key derivation function to use. * @param kdfConfig Configuration for the key derivation function. * @returns 32 byte derived key. */ abstract deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise; } diff --git a/libs/common/src/platform/abstractions/log.service.ts b/libs/common/src/platform/abstractions/log.service.ts index dffa3ca8d3..d77a4f6990 100644 --- a/libs/common/src/platform/abstractions/log.service.ts +++ b/libs/common/src/platform/abstractions/log.service.ts @@ -1,9 +1,9 @@ import { LogLevelType } from "../enums/log-level-type.enum"; export abstract class LogService { - abstract debug(message: string): void; - abstract info(message: string): void; - abstract warning(message: string): void; - abstract error(message: string): void; - abstract write(level: LogLevelType, message: string): void; + abstract debug(message?: any, ...optionalParams: any[]): void; + abstract info(message?: any, ...optionalParams: any[]): void; + abstract warning(message?: any, ...optionalParams: any[]): void; + abstract error(message?: any, ...optionalParams: any[]): void; + abstract write(level: LogLevelType, message?: any, ...optionalParams: any[]): void; } diff --git a/libs/common/src/platform/abstractions/messaging.service.ts b/libs/common/src/platform/abstractions/messaging.service.ts index ab4332c283..f24279f932 100644 --- a/libs/common/src/platform/abstractions/messaging.service.ts +++ b/libs/common/src/platform/abstractions/messaging.service.ts @@ -1,3 +1,3 @@ -export abstract class MessagingService { - abstract send(subscriber: string, arg?: any): void; -} +// Export the new message sender as the legacy MessagingService to minimize changes in the initial PR, +// team specific PR's will come after. +export { MessageSender as MessagingService } from "../messaging/message.sender"; diff --git a/libs/common/src/platform/abstractions/platform-utils.service.ts b/libs/common/src/platform/abstractions/platform-utils.service.ts index d518a17f7b..f2dff46c78 100644 --- a/libs/common/src/platform/abstractions/platform-utils.service.ts +++ b/libs/common/src/platform/abstractions/platform-utils.service.ts @@ -28,6 +28,11 @@ export abstract class PlatformUtilsService { abstract getApplicationVersionNumber(): Promise; abstract supportsWebAuthn(win: Window): boolean; abstract supportsDuo(): boolean; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + * + * Jira: [CL-213](https://bitwarden.atlassian.net/browse/CL-213) + */ abstract showToast( type: "error" | "success" | "warning" | "info", title: string, diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 2348c8844a..f694f32eeb 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -1,12 +1,8 @@ -import { Observable } from "rxjs"; - -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UserId } from "../../types/guid"; -import { KdfType } from "../enums"; import { Account } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; @@ -26,12 +22,9 @@ export type InitOptions = { }; export abstract class StateService { - accounts$: Observable<{ [userId: string]: T }>; - activeAccount$: Observable; - addAccount: (account: T) => Promise; - setActiveUser: (userId: string) => Promise; - clean: (options?: StorageOptions) => Promise; + clearDecryptedData: (userId: UserId) => Promise; + clean: (options?: StorageOptions) => Promise; init: (initOptions?: InitOptions) => Promise; /** @@ -74,14 +67,17 @@ export abstract class StateService { * Used when Lock with MP on Restart is enabled */ setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise; + /** + * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService + */ + setEnableDuckDuckGoBrowserIntegration: ( + value: boolean, + options?: StorageOptions, + ) => Promise; /** * @deprecated For migration purposes only, use getUserKeyMasterKey instead */ getEncryptedCryptoSymmetricKey: (options?: StorageOptions) => Promise; - /** - * @deprecated For migration purposes only, use getUserKeyAuto instead - */ - getCryptoMasterKeyAuto: (options?: StorageOptions) => Promise; /** * @deprecated For migration purposes only, use setUserKeyAuto instead */ @@ -117,8 +113,6 @@ export abstract class StateService { setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; - getEmailVerified: (options?: StorageOptions) => Promise; - setEmailVerified: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegration: (options?: StorageOptions) => Promise; setEnableBrowserIntegration: (value: boolean, options?: StorageOptions) => Promise; getEnableBrowserIntegrationFingerprint: (options?: StorageOptions) => Promise; @@ -141,15 +135,7 @@ export abstract class StateService { * @deprecated For migration purposes only, use setEncryptedUserKeyPin instead */ setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise; - getEverBeenUnlocked: (options?: StorageOptions) => Promise; - setEverBeenUnlocked: (value: boolean, options?: StorageOptions) => Promise; getIsAuthenticated: (options?: StorageOptions) => Promise; - getKdfConfig: (options?: StorageOptions) => Promise; - setKdfConfig: (kdfConfig: KdfConfig, options?: StorageOptions) => Promise; - getKdfType: (options?: StorageOptions) => Promise; - setKdfType: (value: KdfType, options?: StorageOptions) => Promise; - getLastActive: (options?: StorageOptions) => Promise; - setLastActive: (value: number, options?: StorageOptions) => Promise; getLastSync: (options?: StorageOptions) => Promise; setLastSync: (value: string, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; @@ -176,12 +162,9 @@ export abstract class StateService { * Sets the user's Pin, encrypted by the user key */ setProtectedPin: (value: string, options?: StorageOptions) => Promise; - getSecurityStamp: (options?: StorageOptions) => Promise; - setSecurityStamp: (value: string, options?: StorageOptions) => Promise; getUserId: (options?: StorageOptions) => Promise; getVaultTimeout: (options?: StorageOptions) => Promise; setVaultTimeout: (value: number, options?: StorageOptions) => Promise; getVaultTimeoutAction: (options?: StorageOptions) => Promise; setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise; - nextUpActiveUser: () => Promise; } diff --git a/libs/common/src/platform/abstractions/storage.service.ts b/libs/common/src/platform/abstractions/storage.service.ts index f380420c39..390d71ae2a 100644 --- a/libs/common/src/platform/abstractions/storage.service.ts +++ b/libs/common/src/platform/abstractions/storage.service.ts @@ -1,6 +1,6 @@ import { Observable } from "rxjs"; -import { MemoryStorageOptions, StorageOptions } from "../models/domain/storage-options"; +import { StorageOptions } from "../models/domain/storage-options"; export type StorageUpdateType = "save" | "remove"; export type StorageUpdate = { @@ -24,12 +24,3 @@ export abstract class AbstractStorageService { abstract save(key: string, obj: T, options?: StorageOptions): Promise; abstract remove(key: string, options?: StorageOptions): Promise; } - -export abstract class AbstractMemoryStorageService extends AbstractStorageService { - // Used to identify the service in the session sync decorator framework - static readonly TYPE = "MemoryStorageService"; - readonly type = AbstractMemoryStorageService.TYPE; - - abstract get(key: string, options?: MemoryStorageOptions): Promise; - abstract getBypassCache(key: string, options?: MemoryStorageOptions): Promise; -} diff --git a/libs/common/src/platform/enums/kdf-type.enum.ts b/libs/common/src/platform/enums/kdf-type.enum.ts index 97157910f5..fd29bf308c 100644 --- a/libs/common/src/platform/enums/kdf-type.enum.ts +++ b/libs/common/src/platform/enums/kdf-type.enum.ts @@ -1,4 +1,4 @@ -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { RangeWithDefault } from "../misc/range-with-default"; export enum KdfType { @@ -12,4 +12,4 @@ export const ARGON2_ITERATIONS = new RangeWithDefault(2, 10, 3); export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256; export const PBKDF2_ITERATIONS = new RangeWithDefault(600_000, 2_000_000, 600_000); -export const DEFAULT_KDF_CONFIG = new KdfConfig(PBKDF2_ITERATIONS.defaultValue); +export const DEFAULT_KDF_CONFIG = new PBKDF2KdfConfig(PBKDF2_ITERATIONS.defaultValue); diff --git a/libs/common/src/platform/messaging/helpers.spec.ts b/libs/common/src/platform/messaging/helpers.spec.ts new file mode 100644 index 0000000000..fcd36b4411 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.spec.ts @@ -0,0 +1,46 @@ +import { Subject, firstValueFrom } from "rxjs"; + +import { getCommand, isExternalMessage, tagAsExternal } from "./helpers"; +import { Message, CommandDefinition } from "./types"; + +describe("helpers", () => { + describe("getCommand", () => { + it("can get the command from just a string", () => { + const command = getCommand("myCommand"); + + expect(command).toEqual("myCommand"); + }); + + it("can get the command from a message definition", () => { + const commandDefinition = new CommandDefinition("myCommand"); + + const command = getCommand(commandDefinition); + + expect(command).toEqual("myCommand"); + }); + }); + + describe("tag integration", () => { + it("can tag and identify as tagged", async () => { + const messagesSubject = new Subject>(); + + const taggedMessages = messagesSubject.asObservable().pipe(tagAsExternal); + + const firstValuePromise = firstValueFrom(taggedMessages); + + messagesSubject.next({ command: "test" }); + + const result = await firstValuePromise; + + expect(isExternalMessage(result)).toEqual(true); + }); + }); + + describe("isExternalMessage", () => { + it.each([null, { command: "myCommand", test: "object" }, undefined] as Message< + Record + >[])("returns false when value is %s", (value: Message) => { + expect(isExternalMessage(value)).toBe(false); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/helpers.ts b/libs/common/src/platform/messaging/helpers.ts new file mode 100644 index 0000000000..bf119432e0 --- /dev/null +++ b/libs/common/src/platform/messaging/helpers.ts @@ -0,0 +1,23 @@ +import { MonoTypeOperatorFunction, map } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +export const getCommand = (commandDefinition: CommandDefinition | string) => { + if (typeof commandDefinition === "string") { + return commandDefinition; + } else { + return commandDefinition.command; + } +}; + +export const EXTERNAL_SOURCE_TAG = Symbol("externalSource"); + +export const isExternalMessage = (message: Message) => { + return (message as Record)?.[EXTERNAL_SOURCE_TAG] === true; +}; + +export const tagAsExternal: MonoTypeOperatorFunction> = map( + (message: Message) => { + return Object.assign(message, { [EXTERNAL_SOURCE_TAG]: true }); + }, +); diff --git a/libs/common/src/platform/messaging/index.ts b/libs/common/src/platform/messaging/index.ts new file mode 100644 index 0000000000..a9b4eca5ae --- /dev/null +++ b/libs/common/src/platform/messaging/index.ts @@ -0,0 +1,4 @@ +export { MessageListener } from "./message.listener"; +export { MessageSender } from "./message.sender"; +export { Message, CommandDefinition } from "./types"; +export { isExternalMessage } from "./helpers"; diff --git a/libs/common/src/platform/messaging/internal.ts b/libs/common/src/platform/messaging/internal.ts new file mode 100644 index 0000000000..08763d48bc --- /dev/null +++ b/libs/common/src/platform/messaging/internal.ts @@ -0,0 +1,5 @@ +// Built in implementations +export { SubjectMessageSender } from "./subject-message.sender"; + +// Helpers meant to be used only by other implementations +export { tagAsExternal, getCommand } from "./helpers"; diff --git a/libs/common/src/platform/messaging/message.listener.spec.ts b/libs/common/src/platform/messaging/message.listener.spec.ts new file mode 100644 index 0000000000..98bbf1fdc8 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.spec.ts @@ -0,0 +1,47 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { MessageListener } from "./message.listener"; +import { Message, CommandDefinition } from "./types"; + +describe("MessageListener", () => { + const subject = new Subject>(); + const sut = new MessageListener(subject.asObservable()); + + const testCommandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + describe("allMessages$", () => { + it("runs on all nexts", async () => { + const tracker = subscribeTo(sut.allMessages$); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "command1", test: 1 }); + subject.next({ command: "command2", test: 2 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "command1", test: 1 }); + expect(tracker.emissions[1]).toEqual({ command: "command2", test: 2 }); + }); + }); + + describe("messages$", () => { + it("runs on only my commands", async () => { + const tracker = subscribeTo(sut.messages$(testCommandDefinition)); + + const pausePromise = tracker.pauseUntilReceived(2); + + subject.next({ command: "notMyCommand", test: 1 }); + subject.next({ command: "myCommand", test: 2 }); + subject.next({ command: "myCommand", test: 3 }); + subject.next({ command: "notMyCommand", test: 4 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 2 }); + expect(tracker.emissions[1]).toEqual({ command: "myCommand", test: 3 }); + }); + }); +}); diff --git a/libs/common/src/platform/messaging/message.listener.ts b/libs/common/src/platform/messaging/message.listener.ts new file mode 100644 index 0000000000..df453c8422 --- /dev/null +++ b/libs/common/src/platform/messaging/message.listener.ts @@ -0,0 +1,41 @@ +import { EMPTY, Observable, filter } from "rxjs"; + +import { Message, CommandDefinition } from "./types"; + +/** + * A class that allows for listening to messages coming through the application, + * allows for listening of all messages or just the messages you care about. + * + * @note Consider NOT using messaging at all if you can. State Providers offer an observable stream of + * data that is persisted. This can serve messages that might have been used to notify of settings changes + * or vault data changes and those observables should be preferred over messaging. + */ +export class MessageListener { + constructor(private readonly messageStream: Observable>) {} + + /** + * A stream of all messages sent through the application. It does not contain type information for the + * other properties on the messages. You are encouraged to instead subscribe to an individual message + * through {@link messages$}. + */ + allMessages$ = this.messageStream; + + /** + * Creates an observable stream filtered to just the command given via the {@link CommandDefinition} and typed + * to the generic contained in the CommandDefinition. Be careful using this method unless all your messages are being + * sent through `MessageSender.send`, if that isn't the case you should have lower confidence in the message + * payload being the expected type. + * + * @param commandDefinition The CommandDefinition containing the information about the message type you care about. + */ + messages$(commandDefinition: CommandDefinition): Observable { + return this.allMessages$.pipe( + filter((msg) => msg?.command === commandDefinition.command), + ) as Observable; + } + + /** + * A helper property for returning a MessageListener that will never emit any messages and will immediately complete. + */ + static readonly EMPTY = new MessageListener(EMPTY); +} diff --git a/libs/common/src/platform/messaging/message.sender.ts b/libs/common/src/platform/messaging/message.sender.ts new file mode 100644 index 0000000000..6bf2661580 --- /dev/null +++ b/libs/common/src/platform/messaging/message.sender.ts @@ -0,0 +1,62 @@ +import { CommandDefinition } from "./types"; + +class MultiMessageSender implements MessageSender { + constructor(private readonly innerMessageSenders: MessageSender[]) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + for (const messageSender of this.innerMessageSenders) { + messageSender.send(commandDefinition, payload); + } + } +} + +export abstract class MessageSender { + /** + * A method for sending messages in a type safe manner. The passed in command definition + * will require you to provide a compatible type in the payload parameter. + * + * @example + * const MY_COMMAND = new CommandDefinition<{ test: number }>("myCommand"); + * + * this.messageSender.send(MY_COMMAND, { test: 14 }); + * + * @param commandDefinition + * @param payload + */ + abstract send(commandDefinition: CommandDefinition, payload: T): void; + + /** + * A legacy method for sending messages in a non-type safe way. + * + * @remarks Consider defining a {@link CommandDefinition} and passing that in for the first parameter to + * get compilation errors when defining an incompatible payload. + * + * @param command The string based command of your message. + * @param payload Extra contextual information regarding the message. Be aware that this payload may + * be serialized and lose all prototype information. + */ + abstract send(command: string, payload?: object): void; + + /** Implementation of the other two overloads, read their docs instead. */ + abstract send( + commandDefinition: CommandDefinition | string, + payload: T | object, + ): void; + + /** + * A helper method for combine multiple {@link MessageSender}'s. + * @param messageSenders The message senders that should be combined. + * @returns A message sender that will relay all messages to the given message senders. + */ + static combine(...messageSenders: MessageSender[]) { + return new MultiMessageSender(messageSenders); + } + + /** + * A helper property for creating a {@link MessageSender} that sends to nowhere. + */ + static readonly EMPTY: MessageSender = new MultiMessageSender([]); +} diff --git a/libs/common/src/platform/messaging/subject-message.sender.spec.ts b/libs/common/src/platform/messaging/subject-message.sender.spec.ts new file mode 100644 index 0000000000..4278fca7bc --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.spec.ts @@ -0,0 +1,65 @@ +import { Subject } from "rxjs"; + +import { subscribeTo } from "../../../spec/observable-tracker"; + +import { SubjectMessageSender } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +describe("SubjectMessageSender", () => { + const subject = new Subject>(); + const subjectObservable = subject.asObservable(); + + const sut: MessageSender = new SubjectMessageSender(subject); + + describe("send", () => { + it("will send message with command from message definition", async () => { + const commandDefinition = new CommandDefinition<{ test: number }>("myCommand"); + + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send(commandDefinition, { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with command from normal string", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", { test: 1 }); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand", test: 1 }); + }); + + it("will send message with object even if payload not given", async () => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand"); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }); + + it.each([null, undefined])( + "will send message with object even if payload is null-ish (%s)", + async (payloadValue) => { + const tracker = subscribeTo(subjectObservable); + const pausePromise = tracker.pauseUntilReceived(1); + + sut.send("myCommand", payloadValue); + + await pausePromise; + + expect(tracker.emissions[0]).toEqual({ command: "myCommand" }); + }, + ); + }); +}); diff --git a/libs/common/src/platform/messaging/subject-message.sender.ts b/libs/common/src/platform/messaging/subject-message.sender.ts new file mode 100644 index 0000000000..94ae6f27f3 --- /dev/null +++ b/libs/common/src/platform/messaging/subject-message.sender.ts @@ -0,0 +1,17 @@ +import { Subject } from "rxjs"; + +import { getCommand } from "./internal"; +import { MessageSender } from "./message.sender"; +import { Message, CommandDefinition } from "./types"; + +export class SubjectMessageSender implements MessageSender { + constructor(private readonly messagesSubject: Subject>) {} + + send( + commandDefinition: string | CommandDefinition, + payload: object | T = {}, + ): void { + const command = getCommand(commandDefinition); + this.messagesSubject.next(Object.assign(payload ?? {}, { command: command })); + } +} diff --git a/libs/common/src/platform/messaging/types.ts b/libs/common/src/platform/messaging/types.ts new file mode 100644 index 0000000000..f30163344f --- /dev/null +++ b/libs/common/src/platform/messaging/types.ts @@ -0,0 +1,13 @@ +declare const tag: unique symbol; + +/** + * A class for defining information about a message, this is helpful + * alonside `MessageSender` and `MessageListener` for providing a type + * safe(-ish) way of sending and receiving messages. + */ +export class CommandDefinition { + [tag]: T; + constructor(readonly command: string) {} +} + +export type Message = { command: string } & T; diff --git a/libs/common/src/platform/misc/flags.ts b/libs/common/src/platform/misc/flags.ts index cc463b1060..e0089a5451 100644 --- a/libs/common/src/platform/misc/flags.ts +++ b/libs/common/src/platform/misc/flags.ts @@ -10,6 +10,7 @@ export type SharedFlags = { // eslint-disable-next-line @typescript-eslint/ban-types export type SharedDevFlags = { noopNotifications: boolean; + skipWelcomeOnInstall: boolean; }; function getFlags(envFlags: string | T): T { diff --git a/libs/common/src/platform/misc/lazy.spec.ts b/libs/common/src/platform/misc/lazy.spec.ts new file mode 100644 index 0000000000..76ee085d3d --- /dev/null +++ b/libs/common/src/platform/misc/lazy.spec.ts @@ -0,0 +1,85 @@ +import { Lazy } from "./lazy"; + +describe("Lazy", () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("async", () => { + let factory: jest.Mock>; + let lazy: Lazy>; + + beforeEach(() => { + factory = jest.fn(); + lazy = new Lazy(factory); + }); + + describe("get", () => { + it("should call the factory once", async () => { + await lazy.get(); + await lazy.get(); + + expect(factory).toHaveBeenCalledTimes(1); + }); + + it("should return the value from the factory", async () => { + factory.mockResolvedValue(42); + + const value = await lazy.get(); + + expect(value).toBe(42); + }); + }); + + describe("factory throws", () => { + it("should throw the error", async () => { + factory.mockRejectedValue(new Error("factory error")); + + await expect(lazy.get()).rejects.toThrow("factory error"); + }); + }); + + describe("factory returns undefined", () => { + it("should return undefined", async () => { + factory.mockResolvedValue(undefined); + + const value = await lazy.get(); + + expect(value).toBeUndefined(); + }); + }); + + describe("factory returns null", () => { + it("should return null", async () => { + factory.mockResolvedValue(null); + + const value = await lazy.get(); + + expect(value).toBeNull(); + }); + }); + }); + + describe("sync", () => { + const syncFactory = jest.fn(); + let lazy: Lazy; + + beforeEach(() => { + syncFactory.mockReturnValue(42); + lazy = new Lazy(syncFactory); + }); + + it("should return the value from the factory", () => { + const value = lazy.get(); + + expect(value).toBe(42); + }); + + it("should call the factory once", () => { + lazy.get(); + lazy.get(); + + expect(syncFactory).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/libs/common/src/platform/misc/lazy.ts b/libs/common/src/platform/misc/lazy.ts new file mode 100644 index 0000000000..fb85b93678 --- /dev/null +++ b/libs/common/src/platform/misc/lazy.ts @@ -0,0 +1,20 @@ +export class Lazy { + private _value: T | undefined = undefined; + private _isCreated = false; + + constructor(private readonly factory: () => T) {} + + /** + * Resolves the factory and returns the result. Guaranteed to resolve the value only once. + * + * @returns The value produced by your factory. + */ + get(): T { + if (!this._isCreated) { + this._value = this.factory(); + this._isCreated = true; + } + + return this._value as T; + } +} diff --git a/libs/common/src/platform/misc/utils.spec.ts b/libs/common/src/platform/misc/utils.spec.ts index a7a520a77c..964a2a1941 100644 --- a/libs/common/src/platform/misc/utils.spec.ts +++ b/libs/common/src/platform/misc/utils.spec.ts @@ -3,6 +3,33 @@ import * as path from "path"; import { Utils } from "./utils"; describe("Utils Service", () => { + describe("isGuid", () => { + it("is false when null", () => { + expect(Utils.isGuid(null)).toBe(false); + }); + + it("is false when undefined", () => { + expect(Utils.isGuid(undefined)).toBe(false); + }); + + it("is false when empty", () => { + expect(Utils.isGuid("")).toBe(false); + }); + + it("is false when not a string", () => { + expect(Utils.isGuid(123 as any)).toBe(false); + }); + + it("is false when not a guid", () => { + expect(Utils.isGuid("not a guid")).toBe(false); + }); + + it("is true when a guid", () => { + // we use a limited guid scope in which all zeroes is invalid + expect(Utils.isGuid("00000000-0000-1000-8000-000000000000")).toBe(true); + }); + }); + describe("getDomain", () => { it("should fail for invalid urls", () => { expect(Utils.getDomain(null)).toBeNull(); diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts index 83a2da5709..326ed5e8e8 100644 --- a/libs/common/src/platform/misc/utils.ts +++ b/libs/common/src/platform/misc/utils.ts @@ -10,7 +10,7 @@ import { CryptoService } from "../abstractions/crypto.service"; import { EncryptService } from "../abstractions/encrypt.service"; import { I18nService } from "../abstractions/i18n.service"; -const nodeURL = typeof window === "undefined" ? require("url") : null; +const nodeURL = typeof self === "undefined" ? require("url") : null; declare global { /* eslint-disable-next-line no-var */ diff --git a/libs/common/src/platform/models/data/server-config.data.ts b/libs/common/src/platform/models/data/server-config.data.ts index a4819f7567..57e8fbc628 100644 --- a/libs/common/src/platform/models/data/server-config.data.ts +++ b/libs/common/src/platform/models/data/server-config.data.ts @@ -1,5 +1,6 @@ import { Jsonify } from "type-fest"; +import { AllowedFeatureFlagTypes } from "../../../enums/feature-flag.enum"; import { Region } from "../../abstractions/environment.service"; import { ServerConfigResponse, @@ -13,7 +14,7 @@ export class ServerConfigData { server?: ThirdPartyServerConfigData; environment?: EnvironmentServerConfigData; utcDate: string; - featureStates: { [key: string]: string } = {}; + featureStates: { [key: string]: AllowedFeatureFlagTypes } = {}; constructor(serverConfigResponse: Partial) { this.version = serverConfigResponse?.version; diff --git a/libs/common/src/platform/models/domain/account-tokens.spec.ts b/libs/common/src/platform/models/domain/account-tokens.spec.ts deleted file mode 100644 index 733b3908e9..0000000000 --- a/libs/common/src/platform/models/domain/account-tokens.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AccountTokens } from "./account"; - -describe("AccountTokens", () => { - describe("fromJSON", () => { - it("should deserialize to an instance of itself", () => { - expect(AccountTokens.fromJSON({})).toBeInstanceOf(AccountTokens); - }); - }); -}); diff --git a/libs/common/src/platform/models/domain/account.spec.ts b/libs/common/src/platform/models/domain/account.spec.ts index 0c76c16cc2..77c242b6ff 100644 --- a/libs/common/src/platform/models/domain/account.spec.ts +++ b/libs/common/src/platform/models/domain/account.spec.ts @@ -1,4 +1,4 @@ -import { Account, AccountKeys, AccountProfile, AccountSettings, AccountTokens } from "./account"; +import { Account, AccountKeys, AccountProfile, AccountSettings } from "./account"; describe("Account", () => { describe("fromJSON", () => { @@ -10,14 +10,12 @@ describe("Account", () => { const keysSpy = jest.spyOn(AccountKeys, "fromJSON"); const profileSpy = jest.spyOn(AccountProfile, "fromJSON"); const settingsSpy = jest.spyOn(AccountSettings, "fromJSON"); - const tokensSpy = jest.spyOn(AccountTokens, "fromJSON"); Account.fromJSON({}); expect(keysSpy).toHaveBeenCalled(); expect(profileSpy).toHaveBeenCalled(); expect(settingsSpy).toHaveBeenCalled(); - expect(tokensSpy).toHaveBeenCalled(); }); }); }); diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index ae7780ada4..cd416ec1f9 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -126,7 +126,6 @@ export class AccountProfile { name?: string; email?: string; emailVerified?: boolean; - everBeenUnlocked?: boolean; lastSync?: string; userId?: string; kdfIterations?: number; @@ -172,24 +171,11 @@ export class AccountSettings { } } -export class AccountTokens { - securityStamp?: string; - - static fromJSON(obj: Jsonify): AccountTokens { - if (obj == null) { - return null; - } - - return Object.assign(new AccountTokens(), obj); - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); - tokens?: AccountTokens = new AccountTokens(); constructor(init: Partial) { Object.assign(this, { @@ -209,10 +195,6 @@ export class Account { ...new AccountSettings(), ...init?.settings, }, - tokens: { - ...new AccountTokens(), - ...init?.tokens, - }, }); } @@ -226,7 +208,6 @@ export class Account { data: AccountData.fromJSON(json?.data), profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), - tokens: AccountTokens.fromJSON(json?.tokens), }); } } diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index 703a998d1c..cd7cf7d174 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -4,4 +4,5 @@ export class GlobalState { vaultTimeoutAction?: string; enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; + enableDuckDuckGoBrowserIntegration?: boolean; } diff --git a/libs/common/src/platform/models/domain/state.ts b/libs/common/src/platform/models/domain/state.ts index 95557e082a..5dde49f99d 100644 --- a/libs/common/src/platform/models/domain/state.ts +++ b/libs/common/src/platform/models/domain/state.ts @@ -9,9 +9,6 @@ export class State< > { accounts: { [userId: string]: TAccount } = {}; globals: TGlobalState; - activeUserId: string; - authenticatedAccounts: string[] = []; - accountActivity: { [userId: string]: number } = {}; constructor(globals: TGlobalState) { this.globals = globals; diff --git a/libs/common/src/platform/models/domain/storage-options.ts b/libs/common/src/platform/models/domain/storage-options.ts index 6ed430ac50..e27628b850 100644 --- a/libs/common/src/platform/models/domain/storage-options.ts +++ b/libs/common/src/platform/models/domain/storage-options.ts @@ -1,5 +1,3 @@ -import { Jsonify } from "type-fest"; - import { HtmlStorageLocation, StorageLocation } from "../../enums"; export type StorageOptions = { @@ -9,5 +7,3 @@ export type StorageOptions = { htmlStorageLocation?: HtmlStorageLocation; keySuffix?: string; }; - -export type MemoryStorageOptions = StorageOptions & { deserializer?: (obj: Jsonify) => T }; diff --git a/libs/common/src/platform/services/broadcaster.service.ts b/libs/common/src/platform/services/broadcaster.service.ts deleted file mode 100644 index 9d823b00e0..0000000000 --- a/libs/common/src/platform/services/broadcaster.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - BroadcasterService as BroadcasterServiceAbstraction, - MessageBase, -} from "../abstractions/broadcaster.service"; - -export class BroadcasterService implements BroadcasterServiceAbstraction { - subscribers: Map void> = new Map< - string, - (message: MessageBase) => void - >(); - - send(message: MessageBase, id?: string) { - if (id != null) { - if (this.subscribers.has(id)) { - this.subscribers.get(id)(message); - } - return; - } - - this.subscribers.forEach((value) => { - value(message); - }); - } - - subscribe(id: string, messageCallback: (message: MessageBase) => void) { - this.subscribers.set(id, messageCallback); - } - - unsubscribe(id: string) { - if (this.subscribers.has(id)) { - this.subscribers.delete(id); - } - } -} diff --git a/libs/common/src/platform/services/config/default-config.service.ts b/libs/common/src/platform/services/config/default-config.service.ts index e124deccf8..71b76363a3 100644 --- a/libs/common/src/platform/services/config/default-config.service.ts +++ b/libs/common/src/platform/services/config/default-config.service.ts @@ -13,7 +13,11 @@ import { } from "rxjs"; import { SemVer } from "semver"; -import { FeatureFlag, FeatureFlagValue } from "../../../enums/feature-flag.enum"; +import { + DefaultFeatureFlagValue, + FeatureFlag, + FeatureFlagValueType, +} from "../../../enums/feature-flag.enum"; import { UserId } from "../../../types/guid"; import { ConfigApiServiceAbstraction } from "../../abstractions/config/config-api.service.abstraction"; import { ConfigService } from "../../abstractions/config/config.service"; @@ -89,20 +93,21 @@ export class DefaultConfigService implements ConfigService { map((config) => config?.environment?.cloudRegion ?? Region.US), ); } - getFeatureFlag$(key: FeatureFlag, defaultValue?: T) { + + getFeatureFlag$(key: Flag) { return this.serverConfig$.pipe( map((serverConfig) => { if (serverConfig?.featureStates == null || serverConfig.featureStates[key] == null) { - return defaultValue; + return DefaultFeatureFlagValue[key]; } - return serverConfig.featureStates[key] as T; + return serverConfig.featureStates[key] as FeatureFlagValueType; }), ); } - async getFeatureFlag(key: FeatureFlag, defaultValue?: T) { - return await firstValueFrom(this.getFeatureFlag$(key, defaultValue)); + async getFeatureFlag(key: Flag) { + return await firstValueFrom(this.getFeatureFlag$(key)); } checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { diff --git a/libs/common/src/platform/services/console-log.service.spec.ts b/libs/common/src/platform/services/console-log.service.spec.ts index 129969bbc4..508ca4eb32 100644 --- a/libs/common/src/platform/services/console-log.service.spec.ts +++ b/libs/common/src/platform/services/console-log.service.spec.ts @@ -2,13 +2,18 @@ import { interceptConsole, restoreConsole } from "../../../spec"; import { ConsoleLogService } from "./console-log.service"; -let caughtMessage: any; - describe("ConsoleLogService", () => { + const error = new Error("this is an error"); + const obj = { a: 1, b: 2 }; + let consoleSpy: { + log: jest.Mock; + warn: jest.Mock; + error: jest.Mock; + }; let logService: ConsoleLogService; + beforeEach(() => { - caughtMessage = {}; - interceptConsole(caughtMessage); + consoleSpy = interceptConsole(); logService = new ConsoleLogService(true); }); @@ -18,41 +23,41 @@ describe("ConsoleLogService", () => { it("filters messages below the set threshold", () => { logService = new ConsoleLogService(true, () => true); - logService.debug("debug"); - logService.info("info"); - logService.warning("warning"); - logService.error("error"); + logService.debug("debug", error, obj); + logService.info("info", error, obj); + logService.warning("warning", error, obj); + logService.error("error", error, obj); - expect(caughtMessage).toEqual({}); + expect(consoleSpy.log).not.toHaveBeenCalled(); + expect(consoleSpy.warn).not.toHaveBeenCalled(); + expect(consoleSpy.error).not.toHaveBeenCalled(); }); + it("only writes debug messages in dev mode", () => { logService = new ConsoleLogService(false); logService.debug("debug message"); - expect(caughtMessage.log).toBeUndefined(); + expect(consoleSpy.log).not.toHaveBeenCalled(); }); it("writes debug/info messages to console.log", () => { - logService.debug("this is a debug message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is a debug message" }, - }); + logService.debug("this is a debug message", error, obj); + logService.info("this is an info message", error, obj); - logService.info("this is an info message"); - expect(caughtMessage).toMatchObject({ - log: { "0": "this is an info message" }, - }); + expect(consoleSpy.log).toHaveBeenCalledTimes(2); + expect(consoleSpy.log).toHaveBeenCalledWith("this is a debug message", error, obj); + expect(consoleSpy.log).toHaveBeenCalledWith("this is an info message", error, obj); }); + it("writes warning messages to console.warn", () => { - logService.warning("this is a warning message"); - expect(caughtMessage).toMatchObject({ - warn: { 0: "this is a warning message" }, - }); + logService.warning("this is a warning message", error, obj); + + expect(consoleSpy.warn).toHaveBeenCalledWith("this is a warning message", error, obj); }); + it("writes error messages to console.error", () => { - logService.error("this is an error message"); - expect(caughtMessage).toMatchObject({ - error: { 0: "this is an error message" }, - }); + logService.error("this is an error message", error, obj); + + expect(consoleSpy.error).toHaveBeenCalledWith("this is an error message", error, obj); }); }); diff --git a/libs/common/src/platform/services/console-log.service.ts b/libs/common/src/platform/services/console-log.service.ts index 3eb3ad1881..a1480a0c26 100644 --- a/libs/common/src/platform/services/console-log.service.ts +++ b/libs/common/src/platform/services/console-log.service.ts @@ -9,26 +9,26 @@ export class ConsoleLogService implements LogServiceAbstraction { protected filter: (level: LogLevelType) => boolean = null, ) {} - debug(message: string) { + debug(message?: any, ...optionalParams: any[]) { if (!this.isDev) { return; } - this.write(LogLevelType.Debug, message); + this.write(LogLevelType.Debug, message, ...optionalParams); } - info(message: string) { - this.write(LogLevelType.Info, message); + info(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Info, message, ...optionalParams); } - warning(message: string) { - this.write(LogLevelType.Warning, message); + warning(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Warning, message, ...optionalParams); } - error(message: string) { - this.write(LogLevelType.Error, message); + error(message?: any, ...optionalParams: any[]) { + this.write(LogLevelType.Error, message, ...optionalParams); } - write(level: LogLevelType, message: string) { + write(level: LogLevelType, message?: any, ...optionalParams: any[]) { if (this.filter != null && this.filter(level)) { return; } @@ -36,19 +36,19 @@ export class ConsoleLogService implements LogServiceAbstraction { switch (level) { case LogLevelType.Debug: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Info: // eslint-disable-next-line - console.log(message); + console.log(message, ...optionalParams); break; case LogLevelType.Warning: // eslint-disable-next-line - console.warn(message); + console.warn(message, ...optionalParams); break; case LogLevelType.Error: // eslint-disable-next-line - console.error(message); + console.error(message, ...optionalParams); break; default: break; diff --git a/libs/common/src/platform/services/crypto.service.spec.ts b/libs/common/src/platform/services/crypto.service.spec.ts index 16e6d4aa63..d9992adb57 100644 --- a/libs/common/src/platform/services/crypto.service.spec.ts +++ b/libs/common/src/platform/services/crypto.service.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom, of, tap } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeStateProvider } from "../../../spec/fake-state-provider"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { CsprngArray } from "../../types/csprng"; import { UserId } from "../../types/guid"; @@ -37,6 +38,7 @@ describe("cryptoService", () => { const platformUtilService = mock(); const logService = mock(); const stateService = mock(); + const kdfConfigService = mock(); let stateProvider: FakeStateProvider; const mockUserId = Utils.newGuid() as UserId; @@ -58,6 +60,7 @@ describe("cryptoService", () => { stateService, accountService, stateProvider, + kdfConfigService, ); }); @@ -91,21 +94,7 @@ describe("cryptoService", () => { expect(userKey).toEqual(mockUserKey); }); - it("sets from the Auto key if the User Key if not set", async () => { - const autoKeyB64 = - "IT5cA1i5Hncd953pb00E58D2FqJX+fWTj4AvoI67qkGHSQPgulAqKv+LaKRAo9Bg0xzP9Nw00wk4TqjMmGSM+g=="; - stateService.getUserKeyAutoUnlock.mockResolvedValue(autoKeyB64); - const setKeySpy = jest.spyOn(cryptoService, "setUserKey"); - - const userKey = await cryptoService.getUserKey(mockUserId); - - expect(setKeySpy).toHaveBeenCalledWith(expect.any(SymmetricCryptoKey), mockUserId); - expect(setKeySpy).toHaveBeenCalledTimes(1); - - expect(userKey.keyB64).toEqual(autoKeyB64); - }); - - it("returns nullish if there is no auto key and the user key is not set", async () => { + it("returns nullish if the user key is not set", async () => { const userKey = await cryptoService.getUserKey(mockUserId); expect(userKey).toBeFalsy(); @@ -147,17 +136,6 @@ describe("cryptoService", () => { }, ); - describe("hasUserKey", () => { - it.each([true, false])( - "returns %s when the user key is not in memory, but the auto key is set", - async (hasKey) => { - stateProvider.singleUser.getFake(mockUserId, USER_KEY).nextState(null); - cryptoService.hasUserKeyStored = jest.fn().mockResolvedValue(hasKey); - expect(await cryptoService.hasUserKey(mockUserId)).toBe(hasKey); - }, - ); - }); - describe("getUserKeyWithLegacySupport", () => { let mockUserKey: UserKey; let mockMasterKey: MasterKey; diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index c091b6a5a9..873f3ab9a7 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -6,6 +6,7 @@ import { ProfileOrganizationResponse } from "../../admin-console/models/response import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { AccountService } from "../../auth/abstractions/account.service"; +import { KdfConfigService } from "../../auth/abstractions/kdf-config.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { Utils } from "../../platform/misc/utils"; @@ -28,16 +29,7 @@ import { KeyGenerationService } from "../abstractions/key-generation.service"; import { LogService } from "../abstractions/log.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; -import { - KeySuffixOptions, - HashPurpose, - KdfType, - ARGON2_ITERATIONS, - ARGON2_MEMORY, - ARGON2_PARALLELISM, - EncryptionType, - PBKDF2_ITERATIONS, -} from "../enums"; +import { KeySuffixOptions, HashPurpose, EncryptionType } from "../enums"; import { sequentialize } from "../misc/sequentialize"; import { EFFLongWordList } from "../misc/wordlist"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; @@ -91,6 +83,7 @@ export class CryptoService implements CryptoServiceAbstraction { protected stateService: StateService, protected accountService: AccountService, protected stateProvider: StateProvider, + protected kdfConfigService: KdfConfigService, ) { // User Key this.activeUserKeyState = stateProvider.getActive(USER_KEY); @@ -107,7 +100,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_PRIVATE_KEY, { encryptService: this.encryptService, - cryptoService: this, + getUserKey: (userId) => this.getUserKey(userId), }, ); this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null @@ -164,19 +157,8 @@ export class CryptoService implements CryptoServiceAbstraction { } async getUserKey(userId?: UserId): Promise { - let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); - if (userKey) { - return userKey; - } - - // If the user has set their vault timeout to 'Never', we can load the user key from storage - if (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) { - userKey = await this.getKeyFromStorage(KeySuffixOptions.Auto, userId); - if (userKey) { - await this.setUserKey(userKey, userId); - return userKey; - } - } + const userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId)); + return userKey; } async isLegacyUser(masterKey?: MasterKey, userId?: UserId): Promise { @@ -217,10 +199,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (userId == null) { return false; } - return ( - (await this.hasUserKeyInMemory(userId)) || - (await this.hasUserKeyStored(KeySuffixOptions.Auto, userId)) - ); + return await this.hasUserKeyInMemory(userId); } async hasUserKeyInMemory(userId?: UserId): Promise { @@ -297,8 +276,7 @@ export class CryptoService implements CryptoServiceAbstraction { return (masterKey ||= await this.makeMasterKey( password, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), )); } @@ -309,16 +287,10 @@ export class CryptoService implements CryptoServiceAbstraction { * Does not validate the kdf config to ensure it satisfies the minimum requirements for the given kdf type. * TODO: Move to MasterPasswordService */ - async makeMasterKey( - password: string, - email: string, - kdf: KdfType, - KdfConfig: KdfConfig, - ): Promise { + async makeMasterKey(password: string, email: string, KdfConfig: KdfConfig): Promise { return (await this.keyGenerationService.deriveKeyFromPassword( password, email, - kdf, KdfConfig, )) as MasterKey; } @@ -423,12 +395,11 @@ export class CryptoService implements CryptoServiceAbstraction { } async setOrgKeys( - orgs: ProfileOrganizationResponse[] = [], - providerOrgs: ProfileProviderOrganizationResponse[] = [], + orgs: ProfileOrganizationResponse[], + providerOrgs: ProfileProviderOrganizationResponse[], + userId: UserId, ): Promise { - // 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.activeUserEncryptedOrgKeysState.update((_) => { + await this.stateProvider.getUser(userId, USER_ENCRYPTED_ORGANIZATION_KEYS).update(() => { const encOrgKeyData: { [orgId: string]: EncryptedOrganizationKeyData } = {}; orgs.forEach((org) => { @@ -478,8 +449,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_ORGANIZATION_KEYS, null, userId); } - async setProviderKeys(providers: ProfileProviderResponse[]): Promise { - await this.activeUserEncryptedProviderKeysState.update((_) => { + async setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise { + await this.stateProvider.getUser(userId, USER_ENCRYPTED_PROVIDER_KEYS).update(() => { const encProviderKeys: { [providerId: ProviderId]: EncryptedString } = {}; providers.forEach((provider) => { @@ -522,12 +493,14 @@ export class CryptoService implements CryptoServiceAbstraction { return [encShareKey, shareKey as T]; } - async setPrivateKey(encPrivateKey: EncryptedString): Promise { + async setPrivateKey(encPrivateKey: EncryptedString, userId: UserId): Promise { if (encPrivateKey == null) { return; } - await this.activeUserEncryptedPrivateKeyState.update(() => encPrivateKey); + await this.stateProvider + .getUser(userId, USER_ENCRYPTED_PRIVATE_KEY) + .update(() => encPrivateKey); } async getPrivateKey(): Promise { @@ -551,9 +524,10 @@ export class CryptoService implements CryptoServiceAbstraction { return this.hashPhrase(userFingerprint); } - async makeKeyPair(key?: SymmetricCryptoKey): Promise<[string, EncString]> { - // Default to user key - key ||= await this.getUserKeyWithLegacySupport(); + async makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]> { + if (key == null) { + throw new Error("'key' is a required parameter and must be non-null."); + } const keyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048); const publicB64 = Utils.fromBufferToB64(keyPair[0]); @@ -574,8 +548,8 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); } - async makePinKey(pin: string, salt: string, kdf: KdfType, kdfConfig: KdfConfig): Promise { - const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdf, kdfConfig); + async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise { + const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig); return (await this.stretchKey(pinKey)) as PinKey; } @@ -589,7 +563,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptUserKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedUserKey?: EncString, ): Promise { @@ -598,7 +571,7 @@ export class CryptoService implements CryptoServiceAbstraction { if (!pinProtectedUserKey) { throw new Error("No PIN protected key found."); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey); return new SymmetricCryptoKey(userKey) as UserKey; } @@ -607,7 +580,6 @@ export class CryptoService implements CryptoServiceAbstraction { async decryptMasterKeyWithPin( pin: string, salt: string, - kdf: KdfType, kdfConfig: KdfConfig, pinProtectedMasterKey?: EncString, ): Promise { @@ -618,7 +590,7 @@ export class CryptoService implements CryptoServiceAbstraction { } pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString); } - const pinKey = await this.makePinKey(pin, salt, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, salt, kdfConfig); const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey); return new SymmetricCryptoKey(masterKey) as MasterKey; } @@ -651,19 +623,20 @@ export class CryptoService implements CryptoServiceAbstraction { await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId); } - async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise { + async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise { if (publicKey == null) { - publicKey = await this.getPublicKey(); - } - if (publicKey == null) { - throw new Error("Public key unavailable."); + throw new Error("'publicKey' is a required parameter and must be non-null"); } const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1"); return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes)); } - async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise { + async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise { + if (privateKey == null) { + throw new Error("'privateKey' is a required parameter and must be non-null"); + } + const headerPieces = encValue.split("."); let encType: EncryptionType = null; let encPieces: string[]; @@ -695,10 +668,6 @@ export class CryptoService implements CryptoServiceAbstraction { } const data = Utils.fromB64ToArray(encPieces[0]); - const privateKey = privateKeyValue ?? (await this.getPrivateKey()); - if (privateKey == null) { - throw new Error("No private key."); - } let alg: "sha1" | "sha256" = "sha1"; switch (encType) { @@ -768,13 +737,23 @@ export class CryptoService implements CryptoServiceAbstraction { // Can decrypt private key const privateKey = await USER_PRIVATE_KEY.derive([userId, encPrivateKey], { encryptService: this.encryptService, - cryptoService: this, + getUserKey: () => Promise.resolve(key), }); + if (privateKey == null) { + // failed to decrypt + return false; + } + // Can successfully derive public key - await USER_PUBLIC_KEY.derive(privateKey, { + const publicKey = await USER_PUBLIC_KEY.derive(privateKey, { cryptoFunctionService: this.cryptoFunctionService, }); + + if (publicKey == null) { + // failed to decrypt + return false; + } } catch (e) { return false; } @@ -791,6 +770,13 @@ export class CryptoService implements CryptoServiceAbstraction { publicKey: string; privateKey: EncString; }> { + // Verify user key doesn't exist + const existingUserKey = await this.getUserKey(); + if (existingUserKey != null) { + this.logService.error("Tried to initialize account with existing user key."); + throw new Error("Cannot initialize account, keys already exist."); + } + const userKey = (await this.keyGenerationService.createKey(512)) as UserKey; const [publicKey, privateKey] = await this.makeKeyPair(userKey); await this.setUserKey(userKey); @@ -845,8 +831,7 @@ export class CryptoService implements CryptoServiceAbstraction { const pinKey = await this.makePinKey( pin, await this.stateService.getEmail({ userId: userId }), - await this.stateService.getKdfType({ userId: userId }), - await this.stateService.getKdfConfig({ userId: userId }), + await this.kdfConfigService.getKdfConfig(), ); const encPin = await this.encryptService.encrypt(key.key, pinKey); @@ -887,43 +872,6 @@ export class CryptoService implements CryptoServiceAbstraction { return null; } - /** - * Validate that the KDF config follows the requirements for the given KDF type. - * - * @remarks - * Should always be called before updating a users KDF config. - */ - validateKdfConfig(kdf: KdfType, kdfConfig: KdfConfig): void { - switch (kdf) { - case KdfType.PBKDF2_SHA256: - if (!PBKDF2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `PBKDF2 iterations must be between ${PBKDF2_ITERATIONS.min} and ${PBKDF2_ITERATIONS.max}`, - ); - } - break; - case KdfType.Argon2id: - if (!ARGON2_ITERATIONS.inRange(kdfConfig.iterations)) { - throw new Error( - `Argon2 iterations must be between ${ARGON2_ITERATIONS.min} and ${ARGON2_ITERATIONS.max}`, - ); - } - - if (!ARGON2_MEMORY.inRange(kdfConfig.memory)) { - throw new Error( - `Argon2 memory must be between ${ARGON2_MEMORY.min}mb and ${ARGON2_MEMORY.max}mb`, - ); - } - - if (!ARGON2_PARALLELISM.inRange(kdfConfig.parallelism)) { - throw new Error( - `Argon2 parallelism must be between ${ARGON2_PARALLELISM.min} and ${ARGON2_PARALLELISM.max}.`, - ); - } - break; - } - } - protected async clearAllStoredUserKeys(userId?: UserId): Promise { await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); @@ -988,49 +936,19 @@ export class CryptoService implements CryptoServiceAbstraction { } } - async migrateAutoKeyIfNeeded(userId?: UserId) { - const oldAutoKey = await this.stateService.getCryptoMasterKeyAuto({ userId: userId }); - if (!oldAutoKey) { - return; - } - // Decrypt - const masterKey = new SymmetricCryptoKey(Utils.fromB64ToArray(oldAutoKey)) as MasterKey; - if (await this.isLegacyUser(masterKey, userId)) { - // Legacy users don't have a user key, so no need to migrate. - // Instead, set the master key for additional isLegacyUser checks that will log the user out. - userId ??= await firstValueFrom(this.stateProvider.activeUserId$); - await this.masterPasswordService.setMasterKey(masterKey, userId); - return; - } - const encryptedUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ - userId: userId, - }); - const userKey = await this.decryptUserKeyWithMasterKey( - masterKey, - new EncString(encryptedUserKey), - userId, - ); - // Migrate - await this.stateService.setUserKeyAutoUnlock(userKey.keyB64, { userId: userId }); - await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); - // Set encrypted user key in case user immediately locks without syncing - await this.setMasterKeyEncryptedUserKey(encryptedUserKey); - } - async decryptAndMigrateOldPinKey( masterPasswordOnRestart: boolean, pin: string, email: string, - kdf: KdfType, kdfConfig: KdfConfig, oldPinKey: EncString, ): Promise { // Decrypt - const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdf, kdfConfig, oldPinKey); + const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey); const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey(); const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey)); // Migrate - const pinKey = await this.makePinKey(pin, email, kdf, kdfConfig); + const pinKey = await this.makePinKey(pin, email, kdfConfig); const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey); if (masterPasswordOnRestart) { await this.stateService.setDecryptedPinProtected(null); diff --git a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts index 6ac343bcb6..75a571fef2 100644 --- a/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts +++ b/libs/common/src/platform/services/cryptography/multithread-encrypt.service.implementation.ts @@ -19,17 +19,36 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple private clear$ = new Subject(); /** - * Sends items to a web worker to decrypt them. - * This utilises multithreading to decrypt items faster without interrupting other operations (e.g. updating UI). + * Decrypts items using a web worker if the environment supports it. + * Will fall back to the main thread if the window object is not available. */ async decryptItems( items: Decryptable[], key: SymmetricCryptoKey, ): Promise { + if (typeof window === "undefined") { + return super.decryptItems(items, key); + } + if (items == null || items.length < 1) { return []; } + const decryptedItems = await this.getDecryptedItemsFromWorker(items, key); + const parsedItems = JSON.parse(decryptedItems); + + return this.initializeItems(parsedItems); + } + + /** + * Sends items to a web worker to decrypt them. This utilizes multithreading to decrypt items + * faster without interrupting other operations (e.g. updating UI). This method returns values + * prior to deserialization to support forwarding results to another party + */ + async getDecryptedItemsFromWorker( + items: Decryptable[], + key: SymmetricCryptoKey, + ): Promise { this.logService.info("Starting decryption using multithreading"); this.worker ??= new Worker( @@ -53,19 +72,20 @@ export class MultithreadEncryptServiceImplementation extends EncryptServiceImple return await firstValueFrom( fromEvent(this.worker, "message").pipe( filter((response: MessageEvent) => response.data?.id === request.id), - map((response) => JSON.parse(response.data.items)), - map((items) => - items.map((jsonItem: Jsonify) => { - const initializer = getClassInitializer(jsonItem.initializerKey); - return initializer(jsonItem); - }), - ), + map((response) => response.data.items), takeUntil(this.clear$), - defaultIfEmpty([]), + defaultIfEmpty("[]"), ), ); } + protected initializeItems(items: Jsonify[]): T[] { + return items.map((jsonItem: Jsonify) => { + const initializer = getClassInitializer(jsonItem.initializerKey); + return initializer(jsonItem); + }); + } + private clear() { this.clear$.next(); this.worker?.terminate(); diff --git a/libs/common/src/platform/services/default-broadcaster.service.ts b/libs/common/src/platform/services/default-broadcaster.service.ts new file mode 100644 index 0000000000..a16745c643 --- /dev/null +++ b/libs/common/src/platform/services/default-broadcaster.service.ts @@ -0,0 +1,36 @@ +import { Subscription } from "rxjs"; + +import { BroadcasterService, MessageBase } from "../abstractions/broadcaster.service"; +import { MessageListener, MessageSender } from "../messaging"; + +/** + * Temporary implementation that just delegates to the message sender and message listener + * and manages their subscriptions. + */ +export class DefaultBroadcasterService implements BroadcasterService { + subscriptions = new Map(); + + constructor( + private readonly messageSender: MessageSender, + private readonly messageListener: MessageListener, + ) {} + + send(message: MessageBase, id?: string) { + this.messageSender.send(message?.command, message); + } + + subscribe(id: string, messageCallback: (message: MessageBase) => void) { + this.subscriptions.set( + id, + this.messageListener.allMessages$.subscribe((message) => { + messageCallback(message); + }), + ); + } + + unsubscribe(id: string) { + const subscription = this.subscriptions.get(id); + subscription?.unsubscribe(); + this.subscriptions.delete(id); + } +} diff --git a/libs/common/src/platform/services/default-environment.service.spec.ts b/libs/common/src/platform/services/default-environment.service.spec.ts index dd504dc302..7d266e93fc 100644 --- a/libs/common/src/platform/services/default-environment.service.spec.ts +++ b/libs/common/src/platform/services/default-environment.service.spec.ts @@ -31,10 +31,12 @@ describe("EnvironmentService", () => { [testUser]: { name: "name", email: "email", + emailVerified: false, }, [alternateTestUser]: { name: "name", email: "email", + emailVerified: false, }, }); stateProvider = new FakeStateProvider(accountService); @@ -47,6 +49,7 @@ describe("EnvironmentService", () => { id: userId, email: "test@example.com", name: `Test Name ${userId}`, + emailVerified: false, }); await awaitAsync(); }; diff --git a/libs/common/src/platform/services/key-generation.service.spec.ts b/libs/common/src/platform/services/key-generation.service.spec.ts index b3e0aa6d4e..4f04eebd04 100644 --- a/libs/common/src/platform/services/key-generation.service.spec.ts +++ b/libs/common/src/platform/services/key-generation.service.spec.ts @@ -1,9 +1,8 @@ import { mock } from "jest-mock-extended"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; +import { Argon2KdfConfig, PBKDF2KdfConfig } from "../../auth/models/domain/kdf-config"; import { CsprngArray } from "../../types/csprng"; import { CryptoFunctionService } from "../abstractions/crypto-function.service"; -import { KdfType } from "../enums"; import { KeyGenerationService } from "./key-generation.service"; @@ -75,12 +74,11 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using pbkdf2", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.PBKDF2_SHA256; - const kdfConfig = new KdfConfig(600_000); + const kdfConfig = new PBKDF2KdfConfig(600_000); cryptoFunctionService.pbkdf2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); @@ -88,13 +86,12 @@ describe("KeyGenerationService", () => { it("should derive a 32 byte key from a password using argon2id", async () => { const password = "password"; const salt = "salt"; - const kdf = KdfType.Argon2id; - const kdfConfig = new KdfConfig(600_000, 15); + const kdfConfig = new Argon2KdfConfig(3, 16, 4); cryptoFunctionService.hash.mockResolvedValue(new Uint8Array(32)); cryptoFunctionService.argon2.mockResolvedValue(new Uint8Array(32)); - const key = await sut.deriveKeyFromPassword(password, salt, kdf, kdfConfig); + const key = await sut.deriveKeyFromPassword(password, salt, kdfConfig); expect(key.key.length).toEqual(32); }); diff --git a/libs/common/src/platform/services/key-generation.service.ts b/libs/common/src/platform/services/key-generation.service.ts index c592f35e5f..9202b37100 100644 --- a/libs/common/src/platform/services/key-generation.service.ts +++ b/libs/common/src/platform/services/key-generation.service.ts @@ -46,17 +46,16 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction { async deriveKeyFromPassword( password: string | Uint8Array, salt: string | Uint8Array, - kdf: KdfType, kdfConfig: KdfConfig, ): Promise { let key: Uint8Array = null; - if (kdf == null || kdf === KdfType.PBKDF2_SHA256) { + if (kdfConfig.kdfType == null || kdfConfig.kdfType === KdfType.PBKDF2_SHA256) { if (kdfConfig.iterations == null) { kdfConfig.iterations = PBKDF2_ITERATIONS.defaultValue; } key = await this.cryptoFunctionService.pbkdf2(password, salt, "sha256", kdfConfig.iterations); - } else if (kdf == KdfType.Argon2id) { + } else if (kdfConfig.kdfType == KdfType.Argon2id) { if (kdfConfig.iterations == null) { kdfConfig.iterations = ARGON2_ITERATIONS.defaultValue; } diff --git a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts index dd1e4a685e..6b547a491a 100644 --- a/libs/common/src/platform/services/key-state/org-keys.state.spec.ts +++ b/libs/common/src/platform/services/key-state/org-keys.state.spec.ts @@ -65,6 +65,10 @@ describe("derived decrypted org keys", () => { "org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey, }; + const userPrivateKey = makeStaticByteArray(64, 3); + + cryptoService.getPrivateKey.mockResolvedValue(userPrivateKey); + // TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key); cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key); diff --git a/libs/common/src/platform/services/key-state/user-key.state.spec.ts b/libs/common/src/platform/services/key-state/user-key.state.spec.ts index cb758943e5..5c5c5ac70c 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.spec.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.spec.ts @@ -8,7 +8,6 @@ import { EncryptService } from "../../abstractions/encrypt.service"; import { EncryptionType } from "../../enums"; import { Utils } from "../../misc/utils"; import { EncString } from "../../models/domain/enc-string"; -import { CryptoService } from "../crypto.service"; import { USER_ENCRYPTED_PRIVATE_KEY, @@ -89,40 +88,37 @@ describe("Derived decrypted private key", () => { }); it("should derive decrypted private key", async () => { - const cryptoService = mock(); - cryptoService.getUserKey.mockResolvedValue(userKey); + const getUserKey = jest.fn(async () => userKey); const encryptService = mock(); encryptService.decryptToBytes.mockResolvedValue(decryptedPrivateKey); const result = await sut.derive([userId, encryptedPrivateKey], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(decryptedPrivateKey); }); it("should handle null input values", async () => { - const cryptoService = mock(); - cryptoService.getUserKey.mockResolvedValue(userKey); + const getUserKey = jest.fn(async () => userKey); const encryptService = mock(); const result = await sut.derive([userId, null], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(null); }); it("should handle null user key", async () => { - const cryptoService = mock(); - cryptoService.getUserKey.mockResolvedValue(null); + const getUserKey = jest.fn(async () => null); const encryptService = mock(); const result = await sut.derive([userId, encryptedPrivateKey], { encryptService, - cryptoService, + getUserKey, }); expect(result).toEqual(null); diff --git a/libs/common/src/platform/services/key-state/user-key.state.ts b/libs/common/src/platform/services/key-state/user-key.state.ts index 609525b0ac..3df3b2044b 100644 --- a/libs/common/src/platform/services/key-state/user-key.state.ts +++ b/libs/common/src/platform/services/key-state/user-key.state.ts @@ -1,10 +1,10 @@ +import { UserId } from "../../../types/guid"; import { UserPrivateKey, UserPublicKey, UserKey } from "../../../types/key"; import { CryptoFunctionService } from "../../abstractions/crypto-function.service"; import { EncryptService } from "../../abstractions/encrypt.service"; import { EncString, EncryptedString } from "../../models/domain/enc-string"; import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key"; import { CRYPTO_DISK, DeriveDefinition, CRYPTO_MEMORY, UserKeyDefinition } from "../../state"; -import { CryptoService } from "../crypto.service"; export const USER_EVER_HAD_USER_KEY = new UserKeyDefinition( CRYPTO_DISK, @@ -28,15 +28,15 @@ export const USER_PRIVATE_KEY = DeriveDefinition.fromWithUserId< EncryptedString, UserPrivateKey, // TODO: update cryptoService to user key directly - { encryptService: EncryptService; cryptoService: CryptoService } + { encryptService: EncryptService; getUserKey: (userId: UserId) => Promise } >(USER_ENCRYPTED_PRIVATE_KEY, { deserializer: (obj) => new Uint8Array(Object.values(obj)) as UserPrivateKey, - derive: async ([userId, encPrivateKeyString], { encryptService, cryptoService }) => { + derive: async ([userId, encPrivateKeyString], { encryptService, getUserKey }) => { if (encPrivateKeyString == null) { return null; } - const userKey = await cryptoService.getUserKey(userId); + const userKey = await getUserKey(userId); if (userKey == null) { return null; } diff --git a/libs/common/src/platform/services/memory-storage.service.ts b/libs/common/src/platform/services/memory-storage.service.ts index 9cecee7538..d5debf46cc 100644 --- a/libs/common/src/platform/services/memory-storage.service.ts +++ b/libs/common/src/platform/services/memory-storage.service.ts @@ -1,8 +1,8 @@ import { Subject } from "rxjs"; -import { AbstractMemoryStorageService, StorageUpdate } from "../abstractions/storage.service"; +import { AbstractStorageService, StorageUpdate } from "../abstractions/storage.service"; -export class MemoryStorageService extends AbstractMemoryStorageService { +export class MemoryStorageService extends AbstractStorageService { protected store = new Map(); private updatesSubject = new Subject(); @@ -42,8 +42,4 @@ export class MemoryStorageService extends AbstractMemoryStorageService { this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/libs/common/src/platform/services/migration-builder.service.spec.ts b/libs/common/src/platform/services/migration-builder.service.spec.ts index 1330ea07a4..ee9508e8b1 100644 --- a/libs/common/src/platform/services/migration-builder.service.spec.ts +++ b/libs/common/src/platform/services/migration-builder.service.spec.ts @@ -1,6 +1,7 @@ import { mock } from "jest-mock-extended"; import { FakeStorageService } from "../../../spec/fake-storage.service"; +import { ClientType } from "../../enums"; import { MigrationHelper } from "../../state-migrations/migration-helper"; import { MigrationBuilderService } from "./migration-builder.service"; @@ -66,25 +67,38 @@ describe("MigrationBuilderService", () => { global: {}, }; - it.each([ - noAccounts, - nullAndUndefinedAccounts, - emptyAccountObject, - nullCommonAccountProperties, - emptyCommonAccountProperties, - nullGlobal, - undefinedGlobal, - emptyGlobalObject, - ])("should not produce migrations that throw when given data: %s", async (startingState) => { - const sut = new MigrationBuilderService(); + const startingStates = [ + { data: noAccounts, description: "No Accounts" }, + { data: nullAndUndefinedAccounts, description: "Null and Undefined Accounts" }, + { data: emptyAccountObject, description: "Empty Account Object" }, + { data: nullCommonAccountProperties, description: "Null Common Account Properties" }, + { data: emptyCommonAccountProperties, description: "Empty Common Account Properties" }, + { data: nullGlobal, description: "Null Global" }, + { data: undefinedGlobal, description: "Undefined Global" }, + { data: emptyGlobalObject, description: "Empty Global Object" }, + ]; - const helper = new MigrationHelper( - startingStateVersion, - new FakeStorageService(startingState), - mock(), - "general", - ); + const clientTypes = Object.values(ClientType); - await sut.build().migrate(helper); - }); + // Generate all possible test cases + const testCases = startingStates.flatMap((startingState) => + clientTypes.map((clientType) => ({ startingState, clientType })), + ); + + it.each(testCases)( + "should not produce migrations that throw when given $startingState.description for client $clientType", + async ({ startingState, clientType }) => { + const sut = new MigrationBuilderService(); + + const helper = new MigrationHelper( + startingStateVersion, + new FakeStorageService(startingState), + mock(), + "general", + clientType, + ); + + await sut.build().migrate(helper); + }, + ); }); diff --git a/libs/common/src/platform/services/migration-runner.spec.ts b/libs/common/src/platform/services/migration-runner.spec.ts index 3934137f66..fc0d98bc56 100644 --- a/libs/common/src/platform/services/migration-runner.spec.ts +++ b/libs/common/src/platform/services/migration-runner.spec.ts @@ -1,6 +1,7 @@ import { mock } from "jest-mock-extended"; import { awaitAsync } from "../../../spec"; +import { ClientType } from "../../enums"; import { CURRENT_VERSION } from "../../state-migrations"; import { MigrationBuilder } from "../../state-migrations/migration-builder"; import { LogService } from "../abstractions/log.service"; @@ -17,7 +18,7 @@ describe("MigrationRunner", () => { migrationBuilderService.build.mockReturnValue(mockMigrationBuilder); - const sut = new MigrationRunner(storage, logService, migrationBuilderService); + const sut = new MigrationRunner(storage, logService, migrationBuilderService, ClientType.Web); describe("migrate", () => { it("should not run migrations if state is empty", async () => { diff --git a/libs/common/src/platform/services/migration-runner.ts b/libs/common/src/platform/services/migration-runner.ts index 006031f7e5..9e3a6118af 100644 --- a/libs/common/src/platform/services/migration-runner.ts +++ b/libs/common/src/platform/services/migration-runner.ts @@ -1,3 +1,4 @@ +import { ClientType } from "../../enums"; import { waitForMigrations } from "../../state-migrations"; import { CURRENT_VERSION, currentVersion } from "../../state-migrations/migrate"; import { MigrationHelper } from "../../state-migrations/migration-helper"; @@ -11,6 +12,7 @@ export class MigrationRunner { protected diskStorage: AbstractStorageService, protected logService: LogService, protected migrationBuilderService: MigrationBuilderService, + private clientType: ClientType, ) {} async run(): Promise { @@ -19,6 +21,7 @@ export class MigrationRunner { this.diskStorage, this.logService, "general", + this.clientType, ); if (migrationHelper.currentVersion < 0) { diff --git a/libs/common/src/platform/services/noop-messaging.service.ts b/libs/common/src/platform/services/noop-messaging.service.ts deleted file mode 100644 index d1a60bc5bc..0000000000 --- a/libs/common/src/platform/services/noop-messaging.service.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MessagingService } from "../abstractions/messaging.service"; - -export class NoopMessagingService implements MessagingService { - send(subscriber: string, arg: any = {}) { - // Do nothing... - } -} diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 9edc9ed1e3..aa245f8688 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1,9 +1,8 @@ -import { BehaviorSubject } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; import { Jsonify, JsonValue } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { TokenService } from "../../auth/abstractions/token.service"; -import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { BiometricKey } from "../../auth/types/biometric-key"; import { GeneratorOptions } from "../../tools/generator/generator-options"; import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password"; @@ -15,11 +14,8 @@ import { InitOptions, StateService as StateServiceAbstraction, } from "../abstractions/state.service"; -import { - AbstractMemoryStorageService, - AbstractStorageService, -} from "../abstractions/storage.service"; -import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; +import { AbstractStorageService } from "../abstractions/storage.service"; +import { HtmlStorageLocation, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { Account, AccountData, AccountSettings } from "../models/domain/account"; @@ -34,10 +30,7 @@ const keys = { state: "state", stateVersion: "stateVersion", global: "global", - authenticatedAccounts: "authenticatedAccounts", - activeUserId: "activeUserId", tempAccountSettings: "tempAccountSettings", // used to hold account specific settings (i.e clear clipboard) between initial migration and first account authentication - accountActivity: "accountActivity", }; const partialKeys = { @@ -56,31 +49,22 @@ export class StateService< TAccount extends Account = Account, > implements StateServiceAbstraction { - protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({}); - accounts$ = this.accountsSubject.asObservable(); - - protected activeAccountSubject = new BehaviorSubject(null); - activeAccount$ = this.activeAccountSubject.asObservable(); - private hasBeenInited = false; protected isRecoveredSession = false; - protected accountDiskCache = new BehaviorSubject>({}); - // default account serializer, must be overridden by child class protected accountDeserializer = Account.fromJSON as (json: Jsonify) => TAccount; constructor( protected storageService: AbstractStorageService, protected secureStorageService: AbstractStorageService, - protected memoryStorageService: AbstractMemoryStorageService, + protected memoryStorageService: AbstractStorageService, protected logService: LogService, protected stateFactory: StateFactory, protected accountService: AccountService, protected environmentService: EnvironmentService, protected tokenService: TokenService, private migrationRunner: MigrationRunner, - protected useAccountCache: boolean = true, ) {} async init(initOptions: InitOptions = {}): Promise { @@ -115,32 +99,15 @@ export class StateService< return; } + // Get all likely authenticated accounts + const authenticatedAccounts = await firstValueFrom( + this.accountService.accounts$.pipe(map((accounts) => Object.keys(accounts))), + ); + await this.updateState(async (state) => { - state.authenticatedAccounts = - (await this.storageService.get(keys.authenticatedAccounts)) ?? []; - for (const i in state.authenticatedAccounts) { - if (i != null) { - state = await this.syncAccountFromDisk(state.authenticatedAccounts[i]); - } + for (const i in authenticatedAccounts) { + state = await this.syncAccountFromDisk(authenticatedAccounts[i]); } - const storedActiveUser = await this.storageService.get(keys.activeUserId); - if (storedActiveUser != null) { - state.activeUserId = storedActiveUser; - } - await this.pushAccounts(); - this.activeAccountSubject.next(state.activeUserId); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - // account service tracks logged out accounts, but State service does not, so we need to add the active account - // if it's not in the accounts list. - if (state.activeUserId != null && this.accountsSubject.value[state.activeUserId] == null) { - const activeDiskAccount = await this.getAccountFromDisk({ userId: state.activeUserId }); - await this.accountService.addAccount(state.activeUserId as UserId, { - name: activeDiskAccount.profile.name, - email: activeDiskAccount.profile.email, - }); - } - await this.accountService.switchAccount(state.activeUserId as UserId); - // End TODO return state; }); @@ -160,61 +127,24 @@ export class StateService< return state; }); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - // The determination of state should be handled by the various services that control those values. - await this.accountService.addAccount(userId as UserId, { - name: diskAccount.profile.name, - email: diskAccount.profile.email, - }); - return state; } async addAccount(account: TAccount) { await this.environmentService.seedUserEnvironment(account.profile.userId as UserId); await this.updateState(async (state) => { - state.authenticatedAccounts.push(account.profile.userId); - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); state.accounts[account.profile.userId] = account; return state; }); await this.scaffoldNewAccountStorage(account); - await this.setLastActive(new Date().getTime(), { userId: account.profile.userId }); - // TODO: Temporary update to avoid routing all account status changes through account service for now. - await this.accountService.addAccount(account.profile.userId as UserId, { - name: account.profile.name, - email: account.profile.email, - }); - await this.setActiveUser(account.profile.userId); } - async setActiveUser(userId: string): Promise { - await this.clearDecryptedDataForActiveUser(); - await this.updateState(async (state) => { - state.activeUserId = userId; - await this.storageService.save(keys.activeUserId, userId); - this.activeAccountSubject.next(state.activeUserId); - // TODO: temporary update to avoid routing all account status changes through account service for now. - await this.accountService.switchAccount(userId as UserId); - - return state; - }); - - await this.pushAccounts(); - } - - async clean(options?: StorageOptions): Promise { + async clean(options?: StorageOptions): Promise { options = this.reconcileOptions(options, await this.defaultInMemoryOptions()); await this.deAuthenticateAccount(options.userId); - let currentUser = (await this.state())?.activeUserId; - if (options.userId === currentUser) { - currentUser = await this.dynamicallySetActiveUser(); - } await this.removeAccountFromDisk(options?.userId); await this.removeAccountFromMemory(options?.userId); - await this.pushAccounts(); - return currentUser as UserId; } /** @@ -329,23 +259,6 @@ export class StateService< ); } - /** - * @deprecated Use UserKeyAuto instead - */ - async getCryptoMasterKeyAuto(options?: StorageOptions): Promise { - options = this.reconcileOptions( - this.reconcileOptions(options, { keySuffix: "auto" }), - await this.defaultSecureStorageOptions(), - ); - if (options?.userId == null) { - return null; - } - return await this.secureStorageService.get( - `${options.userId}${partialKeys.autoKey}`, - options, - ); - } - /** * @deprecated Use UserKeyAuto instead */ @@ -514,24 +427,6 @@ export class StateService< ); } - async getEmailVerified(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.profile.emailVerified ?? false - ); - } - - async setEmailVerified(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.emailVerified = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getEnableBrowserIntegration(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -571,6 +466,20 @@ export class StateService< ); } + async setEnableDuckDuckGoBrowserIntegration( + value: boolean, + options?: StorageOptions, + ): Promise { + const globals = await this.getGlobals( + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + globals.enableDuckDuckGoBrowserIntegration = value; + await this.saveGlobals( + globals, + this.reconcileOptions(options, await this.defaultOnDiskOptions()), + ); + } + /** * @deprecated Use UserKey instead */ @@ -620,24 +529,6 @@ export class StateService< ); } - async getEverBeenUnlocked(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))) - ?.profile?.everBeenUnlocked ?? false - ); - } - - async setEverBeenUnlocked(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.profile.everBeenUnlocked = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getIsAuthenticated(options?: StorageOptions): Promise { return ( (await this.tokenService.getAccessToken(options?.userId as UserId)) != null && @@ -645,78 +536,6 @@ export class StateService< ); } - async getKdfConfig(options?: StorageOptions): Promise { - const iterations = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfIterations; - const memory = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfMemory; - const parallelism = ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfParallelism; - return new KdfConfig(iterations, memory, parallelism); - } - - async setKdfConfig(config: KdfConfig, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfIterations = config.iterations; - account.profile.kdfMemory = config.memory; - account.profile.kdfParallelism = config.parallelism; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getKdfType(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.profile?.kdfType; - } - - async setKdfType(value: KdfType, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.profile.kdfType = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - - async getLastActive(options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - - const accountActivity = await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - ); - - if (accountActivity == null || Object.keys(accountActivity).length < 1) { - return null; - } - - return accountActivity[options.userId]; - } - - async setLastActive(value: number, options?: StorageOptions): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskOptions()); - if (options.userId == null) { - return; - } - const accountActivity = - (await this.storageService.get<{ [userId: string]: number }>( - keys.accountActivity, - options, - )) ?? {}; - accountActivity[options.userId] = value; - await this.storageService.save(keys.accountActivity, accountActivity, options); - } - async getLastSync(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) @@ -843,23 +662,6 @@ export class StateService< ); } - async getSecurityStamp(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) - )?.tokens?.securityStamp; - } - - async setSecurityStamp(value: string, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - account.tokens.securityStamp = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultInMemoryOptions()), - ); - } - async getUserId(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) @@ -973,32 +775,29 @@ export class StateService< } protected async getAccountFromMemory(options: StorageOptions): Promise { + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); + return await this.state().then(async (state) => { if (state.accounts == null) { return null; } - return state.accounts[await this.getUserIdFromMemory(options)]; - }); - } - - protected async getUserIdFromMemory(options: StorageOptions): Promise { - return await this.state().then((state) => { - return options?.userId != null - ? state.accounts[options.userId]?.profile?.userId - : state.activeUserId; + return state.accounts[userId]; }); } protected async getAccountFromDisk(options: StorageOptions): Promise { - if (options?.userId == null && (await this.state())?.activeUserId == null) { - return null; - } + const userId = + options.userId ?? + (await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + )); - if (this.useAccountCache) { - const cachedAccount = this.accountDiskCache.value[options.userId]; - if (cachedAccount != null) { - return cachedAccount; - } + if (userId == null) { + return null; } const account = options?.useSecureStorage @@ -1008,8 +807,6 @@ export class StateService< this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }), )) : await this.storageService.get(options.userId, options); - - this.setDiskCache(options.userId, account); return account; } @@ -1039,8 +836,6 @@ export class StateService< : this.storageService; await storageLocation.save(`${options.userId}`, account, options); - - this.deleteDiskCache(options.userId); } protected async saveAccountToMemory(account: TAccount): Promise { @@ -1052,7 +847,6 @@ export class StateService< }); }); } - await this.pushAccounts(); } protected async scaffoldNewAccountStorage(account: TAccount): Promise { @@ -1130,17 +924,6 @@ export class StateService< ); } - protected async pushAccounts(): Promise { - await this.state().then((state) => { - if (state.accounts == null || Object.keys(state.accounts).length < 1) { - this.accountsSubject.next({}); - return; - } - - this.accountsSubject.next(state.accounts); - }); - } - protected reconcileOptions( requestedOptions: StorageOptions, defaultOptions: StorageOptions, @@ -1160,53 +943,76 @@ export class StateService< } protected async defaultInMemoryOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Memory, - userId: (await this.state()).activeUserId, + userId, }; } protected async defaultOnDiskOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Session, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskLocalOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Local, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, useSecureStorage: false, }; } protected async defaultOnDiskMemoryOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, htmlStorageLocation: HtmlStorageLocation.Memory, - userId: (await this.state())?.activeUserId ?? (await this.getUserId()), + userId, useSecureStorage: false, }; } protected async defaultSecureStorageOptions(): Promise { + const userId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + return { storageLocation: StorageLocation.Disk, useSecureStorage: true, - userId: (await this.state())?.activeUserId ?? (await this.getActiveUserIdFromStorage()), + userId, }; } protected async getActiveUserIdFromStorage(): Promise { - return await this.storageService.get(keys.activeUserId); + return await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); } protected async removeAccountFromLocalStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskLocalOptions()), ); @@ -1217,7 +1023,10 @@ export class StateService< } protected async removeAccountFromSessionStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + const storedAccount = await this.getAccount( this.reconcileOptions({ userId: userId }, await this.defaultOnDiskOptions()), ); @@ -1228,7 +1037,10 @@ export class StateService< } protected async removeAccountFromSecureStorage(userId: string = null): Promise { - userId = userId ?? (await this.state())?.activeUserId; + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.setUserKeyAutoUnlock(null, { userId: userId }); await this.setUserKeyBiometric(null, { userId: userId }); await this.setCryptoMasterKeyAuto(null, { userId: userId }); @@ -1237,12 +1049,12 @@ export class StateService< } protected async removeAccountFromMemory(userId: string = null): Promise { + userId ??= await firstValueFrom( + this.accountService.activeAccount$.pipe(map((account) => account?.id)), + ); + await this.updateState(async (state) => { - userId = userId ?? state.activeUserId; delete state.accounts[userId]; - - this.deleteDiskCache(userId); - return state; }); } @@ -1255,9 +1067,8 @@ export class StateService< return Object.assign(this.createAccount(), persistentAccountInformation); } - protected async clearDecryptedDataForActiveUser(): Promise { + async clearDecryptedData(userId: UserId): Promise { await this.updateState(async (state) => { - const userId = state?.activeUserId; if (userId != null && state?.accounts[userId]?.data != null) { state.accounts[userId].data = new AccountData(); } @@ -1278,14 +1089,6 @@ export class StateService< // We must have a manual call to clear tokens as we can't leverage state provider to clean // up our data as we have secure storage in the mix. await this.tokenService.clearTokens(userId as UserId); - await this.setLastActive(null, { userId: userId }); - await this.updateState(async (state) => { - state.authenticatedAccounts = state.authenticatedAccounts.filter((id) => id !== userId); - - await this.storageService.save(keys.authenticatedAccounts, state.authenticatedAccounts); - - return state; - }); } protected async removeAccountFromDisk(userId: string) { @@ -1294,32 +1097,6 @@ export class StateService< await this.removeAccountFromSecureStorage(userId); } - async nextUpActiveUser() { - const accounts = (await this.state())?.accounts; - if (accounts == null || Object.keys(accounts).length < 1) { - return null; - } - - let newActiveUser; - for (const userId in accounts) { - if (userId == null) { - continue; - } - if (await this.getIsAuthenticated({ userId: userId })) { - newActiveUser = userId; - break; - } - newActiveUser = null; - } - return newActiveUser as UserId; - } - - protected async dynamicallySetActiveUser() { - const newActiveUser = await this.nextUpActiveUser(); - await this.setActiveUser(newActiveUser); - return newActiveUser; - } - protected async saveSecureStorageKey( key: string, value: T, @@ -1331,9 +1108,10 @@ export class StateService< } protected async state(): Promise> { - const state = await this.memoryStorageService.get>(keys.state, { - deserializer: (s) => State.fromJSON(s, this.accountDeserializer), - }); + let state = await this.memoryStorageService.get>(keys.state); + if (this.memoryStorageService.valuesRequireDeserialization) { + state = State.fromJSON(state, this.accountDeserializer); + } return state; } @@ -1356,20 +1134,6 @@ export class StateService< return await this.setState(updatedState); }); } - - private setDiskCache(key: string, value: TAccount, options?: StorageOptions) { - if (this.useAccountCache) { - this.accountDiskCache.value[key] = value; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } - - protected deleteDiskCache(key: string) { - if (this.useAccountCache) { - delete this.accountDiskCache.value[key]; - this.accountDiskCache.next(this.accountDiskCache.value); - } - } } function withPrototypeForArrayMembers( diff --git a/libs/common/src/platform/services/system.service.ts b/libs/common/src/platform/services/system.service.ts index d19390c45e..80053673d8 100644 --- a/libs/common/src/platform/services/system.service.ts +++ b/libs/common/src/platform/services/system.service.ts @@ -1,10 +1,12 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { firstValueFrom, map, timeout } from "rxjs"; 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 { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; +import { UserId } from "../../types/guid"; import { MessagingService } from "../abstractions/messaging.service"; import { PlatformUtilsService } from "../abstractions/platform-utils.service"; import { StateService } from "../abstractions/state.service"; @@ -25,15 +27,18 @@ export class SystemService implements SystemServiceAbstraction { private autofillSettingsService: AutofillSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, private biometricStateService: BiometricStateService, + private accountService: AccountService, ) {} async startProcessReload(authService: AuthService): Promise { - const accounts = await firstValueFrom(this.stateService.accounts$); + const accounts = await firstValueFrom(this.accountService.accounts$); if (accounts != null) { const keys = Object.keys(accounts); if (keys.length > 0) { for (const userId of keys) { - if ((await authService.getAuthStatus(userId)) === AuthenticationStatus.Unlocked) { + let status = await firstValueFrom(authService.authStatusFor$(userId as UserId)); + status = await authService.getAuthStatus(userId); + if (status === AuthenticationStatus.Unlocked) { return; } } @@ -63,15 +68,24 @@ export class SystemService implements SystemServiceAbstraction { clearInterval(this.reloadInterval); this.reloadInterval = null; - const currentUser = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); + const currentUser = await firstValueFrom( + this.accountService.activeAccount$.pipe( + map((a) => a?.id), + timeout(500), + ), + ); // Replace current active user if they will be logged out on reload if (currentUser != null) { const timeoutAction = await firstValueFrom( this.vaultTimeoutSettingsService.vaultTimeoutAction$().pipe(timeout(500)), ); if (timeoutAction === VaultTimeoutAction.LogOut) { - const nextUser = await this.stateService.nextUpActiveUser(); - await this.stateService.setActiveUser(nextUser); + const nextUser = await firstValueFrom( + this.accountService.nextUpAccount$.pipe(map((account) => account?.id ?? null)), + ); + // Can be removed once we migrate password generation history to state providers + await this.stateService.clearDecryptedData(currentUser); + await this.accountService.switchAccount(nextUser); } } 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 new file mode 100644 index 0000000000..f0d60158c1 --- /dev/null +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts @@ -0,0 +1,71 @@ +import { mock } from "jest-mock-extended"; + +import { CsprngArray } from "../../types/csprng"; +import { UserId } from "../../types/guid"; +import { UserKey } from "../../types/key"; +import { KeySuffixOptions } from "../enums"; +import { Utils } from "../misc/utils"; +import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; + +import { CryptoService } from "./crypto.service"; +import { UserAutoUnlockKeyService } from "./user-auto-unlock-key.service"; + +describe("UserAutoUnlockKeyService", () => { + let userAutoUnlockKeyService: UserAutoUnlockKeyService; + + const mockUserId = Utils.newGuid() as UserId; + + const cryptoService = mock(); + + beforeEach(() => { + userAutoUnlockKeyService = new UserAutoUnlockKeyService(cryptoService); + }); + + describe("setUserKeyInMemoryIfAutoUserKeySet", () => { + it("does nothing if the userId is null", async () => { + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(null); + + // Assert + expect(cryptoService.getUserKeyFromStorage).not.toHaveBeenCalled(); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("does nothing if the autoUserKey is null", async () => { + // Arrange + const userId = mockUserId; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(null); + + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).not.toHaveBeenCalled(); + }); + + it("sets the user key in memory if the autoUserKey is not null", async () => { + // Arrange + const userId = mockUserId; + + const mockRandomBytes = new Uint8Array(64) as CsprngArray; + const mockAutoUserKey: UserKey = new SymmetricCryptoKey(mockRandomBytes) as UserKey; + + cryptoService.getUserKeyFromStorage.mockResolvedValue(mockAutoUserKey); + + // Act + await (userAutoUnlockKeyService as any).setUserKeyInMemoryIfAutoUserKeySet(userId); + + // Assert + expect(cryptoService.getUserKeyFromStorage).toHaveBeenCalledWith( + KeySuffixOptions.Auto, + userId, + ); + expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockAutoUserKey, userId); + }); + }); +}); 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 new file mode 100644 index 0000000000..6c8ce3f048 --- /dev/null +++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts @@ -0,0 +1,36 @@ +import { UserId } from "../../types/guid"; +import { CryptoService } from "../abstractions/crypto.service"; +import { KeySuffixOptions } from "../enums"; + +// TODO: this is a half measure improvement which allows us to reduce some side effects today (cryptoService.getUserKey setting user key in memory if auto key exists) +// but ideally, in the future, we would be able to put this logic into the cryptoService +// after the vault timeout settings service is transitioned to state provider so that +// the getUserKey logic can simply go to the correct location based on the vault timeout settings +// similar to the TokenService (it would either go to secure storage for the auto user key or memory for the user key) + +export class UserAutoUnlockKeyService { + constructor(private cryptoService: CryptoService) {} + + /** + * The presence of the user key in memory dictates whether the user's vault is locked or unlocked. + * However, for users that have the auto unlock user key set, we need to set the user key in memory + * on application bootstrap and on active account changes so that the user's vault loads unlocked. + * @param userId - The user id to check for an auto user key. + */ + async setUserKeyInMemoryIfAutoUserKeySet(userId: UserId): Promise { + if (userId == null) { + return; + } + + const autoUserKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Auto, + userId, + ); + + if (autoUserKey == null) { + return; + } + + await this.cryptoService.setUserKey(autoUserKey, userId); + } +} diff --git a/libs/common/src/platform/state/deserialization-helpers.spec.ts b/libs/common/src/platform/state/deserialization-helpers.spec.ts new file mode 100644 index 0000000000..b1ae447997 --- /dev/null +++ b/libs/common/src/platform/state/deserialization-helpers.spec.ts @@ -0,0 +1,25 @@ +import { record } from "./deserialization-helpers"; + +describe("deserialization helpers", () => { + describe("record", () => { + it("deserializes a record when keys are strings", () => { + const deserializer = record((value: number) => value); + const input = { + a: 1, + b: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + + it("deserializes a record when keys are numbers", () => { + const deserializer = record((value: number) => value); + const input = { + 1: 1, + 2: 2, + }; + const output = deserializer(input); + expect(output).toEqual(input); + }); + }); +}); diff --git a/libs/common/src/platform/state/deserialization-helpers.ts b/libs/common/src/platform/state/deserialization-helpers.ts index d68a3d0444..8fd5d2da19 100644 --- a/libs/common/src/platform/state/deserialization-helpers.ts +++ b/libs/common/src/platform/state/deserialization-helpers.ts @@ -21,7 +21,7 @@ export function array( * * @param valueDeserializer */ -export function record( +export function record( valueDeserializer: (value: Jsonify) => T, ): (record: Jsonify>) => Record { return (jsonValue: Jsonify | null>) => { @@ -29,10 +29,10 @@ export function record( return null; } - const output: Record = {}; - for (const key in jsonValue) { - output[key] = valueDeserializer((jsonValue as Record>)[key]); - } + const output: Record = {} as any; + Object.entries(jsonValue).forEach(([key, value]) => { + output[key as TKey] = valueDeserializer(value); + }); return output; }; } diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts index c1cc15a176..681963f823 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.provider.spec.ts @@ -1,7 +1,6 @@ import { mock } from "jest-mock-extended"; import { mockAccountServiceWith, trackEmissions } from "../../../../spec"; -import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { UserId } from "../../../types/guid"; import { SingleUserStateProvider } from "../user-state.provider"; @@ -14,7 +13,7 @@ describe("DefaultActiveUserStateProvider", () => { id: userId, name: "name", email: "email", - status: AuthenticationStatus.Locked, + emailVerified: false, }; const accountService = mockAccountServiceWith(userId, accountInfo); let sut: DefaultActiveUserStateProvider; diff --git a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts index 51a972a9dc..c652136a0d 100644 --- a/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-active-user-state.spec.ts @@ -82,6 +82,7 @@ describe("DefaultActiveUserState", () => { activeAccountSubject.next({ id: userId, email: `test${id}@example.com`, + emailVerified: false, name: `Test User ${id}`, }); await awaitAsync(); diff --git a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts index 48ccb9d300..3c8c39e21e 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.provider.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.provider.ts @@ -1,10 +1,6 @@ import { Observable } from "rxjs"; import { DerivedStateDependencies } from "../../../types/state"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; import { DerivedStateProvider } from "../derived-state.provider"; @@ -14,7 +10,7 @@ import { DefaultDerivedState } from "./default-derived-state"; export class DefaultDerivedStateProvider implements DerivedStateProvider { private cache: Record> = {}; - constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {} + constructor() {} get( parentState$: Observable, @@ -39,11 +35,6 @@ export class DefaultDerivedStateProvider implements DerivedStateProvider { deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { - return new DefaultDerivedState( - parentState$, - deriveDefinition, - this.memoryStorage, - dependencies, - ); + return new DefaultDerivedState(parentState$, deriveDefinition, dependencies); } } diff --git a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts index 958a938611..7e8d76bd20 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.spec.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.spec.ts @@ -5,7 +5,6 @@ import { Subject, firstValueFrom } from "rxjs"; import { awaitAsync, trackEmissions } from "../../../../spec"; -import { FakeStorageService } from "../../../../spec/fake-storage.service"; import { DeriveDefinition } from "../derive-definition"; import { StateDefinition } from "../state-definition"; @@ -29,7 +28,6 @@ const deriveDefinition = new DeriveDefinition( describe("DefaultDerivedState", () => { let parentState$: Subject; - let memoryStorage: FakeStorageService; let sut: DefaultDerivedState; const deps = { date: new Date(), @@ -38,8 +36,7 @@ describe("DefaultDerivedState", () => { beforeEach(() => { callCount = 0; parentState$ = new Subject(); - memoryStorage = new FakeStorageService(); - sut = new DefaultDerivedState(parentState$, deriveDefinition, memoryStorage, deps); + sut = new DefaultDerivedState(parentState$, deriveDefinition, deps); }); afterEach(() => { @@ -66,71 +63,33 @@ describe("DefaultDerivedState", () => { expect(callCount).toBe(1); }); - it("should store the derived state in memory", async () => { - const dateString = "2020-01-01"; - trackEmissions(sut.state$); - parentState$.next(dateString); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(new Date(dateString)), - ); - const calls = memoryStorage.mock.save.mock.calls; - expect(calls.length).toBe(1); - expect(calls[0][0]).toBe(deriveDefinition.buildCacheKey()); - expect(calls[0][1]).toEqual(derivedValue(new Date(dateString))); - }); - describe("forceValue", () => { const initialParentValue = "2020-01-01"; const forced = new Date("2020-02-02"); let emissions: Date[]; - describe("without observers", () => { - beforeEach(async () => { - parentState$.next(initialParentValue); - await awaitAsync(); - }); - - it("should store the forced value", async () => { - await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(forced), - ); - }); + beforeEach(async () => { + emissions = trackEmissions(sut.state$); + parentState$.next(initialParentValue); + await awaitAsync(); }); - describe("with observers", () => { - beforeEach(async () => { - emissions = trackEmissions(sut.state$); - parentState$.next(initialParentValue); - await awaitAsync(); - }); + it("should force the value", async () => { + await sut.forceValue(forced); + expect(emissions).toEqual([new Date(initialParentValue), forced]); + }); - it("should store the forced value", async () => { - await sut.forceValue(forced); - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(forced), - ); - }); + it("should only force the value once", async () => { + await sut.forceValue(forced); - it("should force the value", async () => { - await sut.forceValue(forced); - expect(emissions).toEqual([new Date(initialParentValue), forced]); - }); + parentState$.next(initialParentValue); + await awaitAsync(); - it("should only force the value once", async () => { - await sut.forceValue(forced); - - parentState$.next(initialParentValue); - await awaitAsync(); - - expect(emissions).toEqual([ - new Date(initialParentValue), - forced, - new Date(initialParentValue), - ]); - }); + expect(emissions).toEqual([ + new Date(initialParentValue), + forced, + new Date(initialParentValue), + ]); }); }); @@ -148,42 +107,6 @@ describe("DefaultDerivedState", () => { expect(parentState$.observed).toBe(false); }); - it("should clear state after cleanup", async () => { - const subscription = sut.state$.subscribe(); - parentState$.next(newDate); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(new Date(newDate)), - ); - - subscription.unsubscribe(); - // Wait for cleanup - await awaitAsync(cleanupDelayMs * 2); - - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toBeUndefined(); - }); - - it("should not clear state after cleanup if clearOnCleanup is false", async () => { - deriveDefinition.options.clearOnCleanup = false; - - const subscription = sut.state$.subscribe(); - parentState$.next(newDate); - await awaitAsync(); - - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(new Date(newDate)), - ); - - subscription.unsubscribe(); - // Wait for cleanup - await awaitAsync(cleanupDelayMs * 2); - - expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual( - derivedValue(new Date(newDate)), - ); - }); - it("should not cleanup if there are still subscribers", async () => { const subscription1 = sut.state$.subscribe(); const sub2Emissions: Date[] = []; @@ -260,7 +183,3 @@ describe("DefaultDerivedState", () => { }); }); }); - -function derivedValue(value: T) { - return { derived: true, value }; -} diff --git a/libs/common/src/platform/state/implementations/default-derived-state.ts b/libs/common/src/platform/state/implementations/default-derived-state.ts index 657df2bfdf..9abb299809 100644 --- a/libs/common/src/platform/state/implementations/default-derived-state.ts +++ b/libs/common/src/platform/state/implementations/default-derived-state.ts @@ -1,10 +1,6 @@ import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs"; import { DerivedStateDependencies } from "../../../types/state"; -import { - AbstractStorageService, - ObservableStorageService, -} from "../../abstractions/storage.service"; import { DeriveDefinition } from "../derive-definition"; import { DerivedState } from "../derived-state"; @@ -22,7 +18,6 @@ export class DefaultDerivedState, protected deriveDefinition: DeriveDefinition, - private memoryStorage: AbstractStorageService & ObservableStorageService, private dependencies: TDeps, ) { this.storageKey = deriveDefinition.storageKey; @@ -34,7 +29,6 @@ export class DefaultDerivedState { return new ReplaySubject(1); }, - resetOnRefCountZero: () => - timer(this.deriveDefinition.cleanupDelayMs).pipe( - concatMap(async () => { - if (this.deriveDefinition.clearOnCleanup) { - await this.memoryStorage.remove(this.storageKey); - } - return true; - }), - ), + resetOnRefCountZero: () => timer(this.deriveDefinition.cleanupDelayMs), }), ); } async forceValue(value: TTo) { - await this.storeValue(value); this.forcedValueSubject.next(value); return value; } - - private storeValue(value: TTo) { - return this.memoryStorage.save(this.storageKey, { derived: true, value }); - } } diff --git a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts index 3243b53d67..98d423cf48 100644 --- a/libs/common/src/platform/state/implementations/default-state.provider.spec.ts +++ b/libs/common/src/platform/state/implementations/default-state.provider.spec.ts @@ -69,7 +69,12 @@ describe("DefaultStateProvider", () => { userId?: UserId, ) => Observable, ) => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); @@ -114,7 +119,12 @@ describe("DefaultStateProvider", () => { ); describe("getUserState$", () => { - const accountInfo = { email: "email", name: "name", status: AuthenticationStatus.LoggedOut }; + const accountInfo = { + email: "email", + emailVerified: false, + name: "name", + status: AuthenticationStatus.LoggedOut, + }; const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", { deserializer: (s) => s, }); diff --git a/libs/common/src/platform/state/key-definition.ts b/libs/common/src/platform/state/key-definition.ts index b2a8ff8712..bdabd8df50 100644 --- a/libs/common/src/platform/state/key-definition.ts +++ b/libs/common/src/platform/state/key-definition.ts @@ -113,7 +113,7 @@ export class KeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 15dc9ff757..f1e7dc80ab 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -24,8 +24,10 @@ export type ClientLocations = { web: StorageLocation | "disk-local"; /** * Overriding storage location for browser clients. + * + * "memory-large-object" is used to store non-countable objects in memory. This exists due to limited persistent memory available to browser extensions. */ - //browser: StorageLocation; + browser: StorageLocation | "memory-large-object"; /** * Overriding storage location for desktop clients. */ diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 18df252062..6b309ecfb9 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -35,10 +35,13 @@ export const BILLING_DISK = new StateDefinition("billing", "disk"); // Auth +export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const ACCOUNT_MEMORY = new StateDefinition("account", "memory"); +export const ACCOUNT_DISK = new StateDefinition("account", "disk"); export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory"); export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk"); +export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory"); export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" }); export const ROUTER_DISK = new StateDefinition("router", "disk"); export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { @@ -116,7 +119,9 @@ export const EVENT_COLLECTION_DISK = new StateDefinition("eventCollection", "dis export const SEND_DISK = new StateDefinition("encryptedSend", "disk", { web: "memory", }); -export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory"); +export const SEND_MEMORY = new StateDefinition("decryptedSend", "memory", { + browser: "memory-large-object", +}); // Vault @@ -133,10 +138,16 @@ export const VAULT_ONBOARDING = new StateDefinition("vaultOnboarding", "disk", { export const VAULT_SETTINGS_DISK = new StateDefinition("vaultSettings", "disk", { web: "disk-local", }); -export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory"); -export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory"); +export const VAULT_BROWSER_MEMORY = new StateDefinition("vaultBrowser", "memory", { + browser: "memory-large-object", +}); +export const VAULT_SEARCH_MEMORY = new StateDefinition("vaultSearch", "memory", { + browser: "memory-large-object", +}); export const CIPHERS_DISK = new StateDefinition("ciphers", "disk", { web: "memory" }); export const CIPHERS_DISK_LOCAL = new StateDefinition("ciphersLocal", "disk", { web: "disk-local", }); -export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory"); +export const CIPHERS_MEMORY = new StateDefinition("ciphersMemory", "memory", { + browser: "memory-large-object", +}); diff --git a/libs/common/src/platform/state/storage/memory-storage.service.ts b/libs/common/src/platform/state/storage/memory-storage.service.ts index 36116f5e4e..ab45c101f9 100644 --- a/libs/common/src/platform/state/storage/memory-storage.service.ts +++ b/libs/common/src/platform/state/storage/memory-storage.service.ts @@ -1,13 +1,13 @@ import { Subject } from "rxjs"; import { - AbstractMemoryStorageService, + AbstractStorageService, ObservableStorageService, StorageUpdate, } from "../../abstractions/storage.service"; export class MemoryStorageService - extends AbstractMemoryStorageService + extends AbstractStorageService implements ObservableStorageService { protected store: Record = {}; @@ -49,8 +49,4 @@ export class MemoryStorageService this.updatesSubject.next({ key, updateType: "remove" }); return Promise.resolve(); } - - getBypassCache(key: string): Promise { - return this.get(key); - } } diff --git a/libs/common/src/platform/state/user-key-definition.ts b/libs/common/src/platform/state/user-key-definition.ts index 3eb9369080..4c622e29f1 100644 --- a/libs/common/src/platform/state/user-key-definition.ts +++ b/libs/common/src/platform/state/user-key-definition.ts @@ -120,7 +120,7 @@ export class UserKeyDefinition { * }); * ``` */ - static record( + static record( stateDefinition: StateDefinition, key: string, // We have them provide options for the value of the record, depending on future options we add, this could get a little weird. diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index e8135f3d6c..84fa7bd077 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -118,6 +118,7 @@ import { EnvironmentService } from "../platform/abstractions/environment.service import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; +import { UserId } from "../types/guid"; import { AttachmentRequest } from "../vault/models/request/attachment.request"; import { CipherBulkDeleteRequest } from "../vault/models/request/cipher-bulk-delete.request"; import { CipherBulkMoveRequest } from "../vault/models/request/cipher-bulk-move.request"; @@ -1423,8 +1424,8 @@ export class ApiService implements ApiServiceAbstraction { return new ListResponse(r, EventResponse); } - async postEventsCollect(request: EventRequest[]): Promise { - const authHeader = await this.getActiveBearerToken(); + async postEventsCollect(request: EventRequest[], userId?: UserId): Promise { + const authHeader = await this.tokenService.getAccessToken(userId); const headers = new Headers({ "Device-Type": this.deviceType, Authorization: "Bearer " + authHeader, diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index 641c1b4d44..1482bb8b61 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -36,7 +36,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const userId = await firstValueFrom(this.stateProvider.activeUserId$); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(cipherId, organizationId))) { + if (!(await this.shouldUpdate(cipherId, organizationId, eventType))) { return; } @@ -64,6 +64,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction private async shouldUpdate( cipherId: string = null, organizationId: string = null, + eventType: EventType = null, ): Promise { const orgIds$ = this.organizationService.organizations$.pipe( map((orgs) => orgs?.filter((o) => o.useEvents)?.map((x) => x.id) ?? []), @@ -85,6 +86,11 @@ export class EventCollectionService implements EventCollectionServiceAbstraction return false; } + // Individual vault export doesn't need cipher id or organization id. + if (eventType == EventType.User_ClientExportedVault) { + return true; + } + // If the cipher is null there must be an organization id provided if (cipher == null && organizationId == null) { return false; diff --git a/libs/common/src/services/event/event-upload.service.ts b/libs/common/src/services/event/event-upload.service.ts index 6f229751bf..c87d3b2024 100644 --- a/libs/common/src/services/event/event-upload.service.ts +++ b/libs/common/src/services/event/event-upload.service.ts @@ -70,7 +70,7 @@ export class EventUploadService implements EventUploadServiceAbstraction { return req; }); try { - await this.apiService.postEventsCollect(request); + await this.apiService.postEventsCollect(request, userId); } catch (e) { this.logService.error(e); // Add the events back to state if there was an error and they were not uploaded. 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 a8afc63297..4fac3be9c9 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 @@ -172,7 +172,6 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } async clear(userId?: string): Promise { - await this.stateService.setEverBeenUnlocked(false, { userId: userId }); await this.cryptoService.clearPinKeys(userId); } 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 243b644dd8..14b26fa541 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 @@ -1,19 +1,18 @@ import { MockProxy, any, mock } from "jest-mock-extended"; -import { BehaviorSubject } from "rxjs"; +import { BehaviorSubject, from, of } from "rxjs"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; +import { AccountInfo } from "../../auth/abstractions/account.service"; 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 { CryptoService } from "../../platform/abstractions/crypto.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 { Account } from "../../platform/models/domain/account"; import { StateEventRunnerService } from "../../platform/state"; import { UserId } from "../../types/guid"; import { CipherService } from "../../vault/abstractions/cipher.service"; @@ -28,7 +27,6 @@ describe("VaultTimeoutService", () => { let cipherService: MockProxy; let folderService: MockProxy; let collectionService: MockProxy; - let cryptoService: MockProxy; let platformUtilsService: MockProxy; let messagingService: MockProxy; let searchService: MockProxy; @@ -39,7 +37,6 @@ describe("VaultTimeoutService", () => { let lockedCallback: jest.Mock, [userId: string]>; let loggedOutCallback: jest.Mock, [expired: boolean, userId?: string]>; - let accountsSubject: BehaviorSubject>; let vaultTimeoutActionSubject: BehaviorSubject; let availableVaultTimeoutActionsSubject: BehaviorSubject; @@ -53,7 +50,6 @@ describe("VaultTimeoutService", () => { cipherService = mock(); folderService = mock(); collectionService = mock(); - cryptoService = mock(); platformUtilsService = mock(); messagingService = mock(); searchService = mock(); @@ -65,10 +61,6 @@ describe("VaultTimeoutService", () => { lockedCallback = jest.fn(); loggedOutCallback = jest.fn(); - accountsSubject = new BehaviorSubject(null); - - stateService.accounts$ = accountsSubject; - vaultTimeoutActionSubject = new BehaviorSubject(VaultTimeoutAction.Lock); vaultTimeoutSettingsService.vaultTimeoutAction$.mockReturnValue(vaultTimeoutActionSubject); @@ -81,7 +73,6 @@ describe("VaultTimeoutService", () => { cipherService, folderService, collectionService, - cryptoService, platformUtilsService, messagingService, searchService, @@ -115,6 +106,13 @@ describe("VaultTimeoutService", () => { // Both are available by default and the specific test can change this per test availableVaultTimeoutActionsSubject.next([VaultTimeoutAction.Lock, VaultTimeoutAction.LogOut]); + authService.authStatusFor$.mockImplementation((userId) => { + return from([ + accounts[userId]?.authStatus ?? AuthenticationStatus.LoggedOut, + AuthenticationStatus.Locked, + ]); + }); + authService.getAuthStatus.mockImplementation((userId) => { return Promise.resolve(accounts[userId]?.authStatus); }); @@ -127,21 +125,39 @@ describe("VaultTimeoutService", () => { return Promise.resolve(accounts[userId]?.vaultTimeout); }); - stateService.getLastActive.mockImplementation((options) => { - return Promise.resolve(accounts[options.userId]?.lastActive); - }); - stateService.getUserId.mockResolvedValue(globalSetups?.userId); - stateService.activeAccount$ = new BehaviorSubject(globalSetups?.userId); - + // Set desired user active and known users on accounts service : note the only thing that matters here is that the ID are set if (globalSetups?.userId) { accountService.activeAccountSubject.next({ id: globalSetups.userId as UserId, email: null, + emailVerified: false, name: null, }); } + accountService.accounts$ = of( + Object.entries(accounts).reduce( + (agg, [id]) => { + agg[id] = { + email: "", + emailVerified: true, + name: "", + }; + return agg; + }, + {} as Record, + ), + ); + accountService.accountActivity$ = of( + Object.entries(accounts).reduce( + (agg, [id, info]) => { + agg[id] = info.lastActive ? new Date(info.lastActive) : null; + return agg; + }, + {} as Record, + ), + ); platformUtilsService.isViewOpen.mockResolvedValue(globalSetups?.isViewOpen ?? false); @@ -158,23 +174,12 @@ describe("VaultTimeoutService", () => { ], ); }); - - const accountsSubjectValue: Record = Object.keys(accounts).reduce( - (agg, key) => { - const newPartial: Record = {}; - newPartial[key] = null; // No values actually matter on this other than the key - return Object.assign(agg, newPartial); - }, - {} as Record, - ); - accountsSubject.next(accountsSubjectValue); }; const expectUserToHaveLocked = (userId: string) => { // This does NOT assert all the things that the lock process does expect(stateService.getIsAuthenticated).toHaveBeenCalledWith({ userId: userId }); expect(vaultTimeoutSettingsService.availableVaultTimeoutActions$).toHaveBeenCalledWith(userId); - expect(stateService.setEverBeenUnlocked).toHaveBeenCalledWith(true, { userId: userId }); expect(stateService.setUserKeyAutoUnlock).toHaveBeenCalledWith(null, { userId: userId }); expect(masterPasswordService.mock.clearMasterKey).toHaveBeenCalledWith(userId); expect(cipherService.clearCache).toHaveBeenCalledWith(userId); @@ -389,18 +394,6 @@ describe("VaultTimeoutService", () => { expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user1"); }); - it("should call messaging service locked message if no user passed into lock", async () => { - setupLock(); - - await vaultTimeoutService.lock(); - - // Currently these pass `undefined` (or what they were given) as the userId back - // but we could change this to give the user that was locked (active) to these methods - // so they don't have to get it their own way, but that is a behavioral change that needs - // to be tested. - expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: undefined }); - }); - it("should call locked callback if no user passed into lock", async () => { setupLock(); @@ -416,25 +409,31 @@ describe("VaultTimeoutService", () => { it("should call state event runner with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", "user2"); + await vaultTimeoutService.lock(user2); + + expect(stateEventRunnerService.handleEvent).toHaveBeenCalledWith("lock", user2); }); it("should call messaging service locked message with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: "user2" }); + await vaultTimeoutService.lock(user2); + + expect(messagingService.send).toHaveBeenCalledWith("locked", { userId: user2 }); }); it("should call locked callback with user passed into lock", async () => { setupLock(); - await vaultTimeoutService.lock("user2"); + const user2 = "user2" as UserId; - expect(lockedCallback).toHaveBeenCalledWith("user2"); + await vaultTimeoutService.lock(user2); + + expect(lockedCallback).toHaveBeenCalledWith(user2); }); }); }); 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 35faf0fcee..a75fb6d4c4 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -1,4 +1,4 @@ -import { firstValueFrom, timeout } from "rxjs"; +import { combineLatest, filter, firstValueFrom, map, switchMap, timeout } from "rxjs"; import { SearchService } from "../../abstractions/search.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; @@ -7,9 +7,7 @@ import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { InternalMasterPasswordServiceAbstraction } from "../../auth/abstractions/master-password.service.abstraction"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; -import { ClientType } from "../../enums"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; -import { CryptoService } from "../../platform/abstractions/crypto.service"; import { MessagingService } from "../../platform/abstractions/messaging.service"; import { PlatformUtilsService } from "../../platform/abstractions/platform-utils.service"; import { StateService } from "../../platform/abstractions/state.service"; @@ -28,7 +26,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private cipherService: CipherService, private folderService: FolderService, private collectionService: CollectionService, - private cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, private messagingService: MessagingService, private searchService: SearchService, @@ -44,8 +41,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (this.inited) { return; } - // TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3483) - await this.migrateKeyForNeverLockIfNeeded(); this.inited = true; if (checkOnInterval) { @@ -64,17 +59,28 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { // Get whether or not the view is open a single time so it can be compared for each user const isViewOpen = await this.platformUtilsService.isViewOpen(); - const activeUserId = await firstValueFrom(this.stateService.activeAccount$.pipe(timeout(500))); - - const accounts = await firstValueFrom(this.stateService.accounts$); - for (const userId in accounts) { - if (userId != null && (await this.shouldLock(userId, activeUserId, isViewOpen))) { - await this.executeTimeoutAction(userId); - } - } + await firstValueFrom( + combineLatest([ + this.accountService.activeAccount$, + this.accountService.accountActivity$, + ]).pipe( + switchMap(async ([activeAccount, accountActivity]) => { + const activeUserId = activeAccount?.id; + for (const userIdString in accountActivity) { + const userId = userIdString as UserId; + if ( + userId != null && + (await this.shouldLock(userId, accountActivity[userId], activeUserId, isViewOpen)) + ) { + await this.executeTimeoutAction(userId); + } + } + }), + ), + ); } - async lock(userId?: string): Promise { + async lock(userId?: UserId): Promise { const authed = await this.stateService.getIsAuthenticated({ userId: userId }); if (!authed) { return; @@ -88,7 +94,27 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.logOut(userId); } - const currentUserId = (await firstValueFrom(this.accountService.activeAccount$)).id; + const currentUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + + const lockingUserId = userId ?? currentUserId; + + // HACK: Start listening for the transition of the locking user from something to the locked state. + // This is very much a hack to ensure that the authentication status to retrievable right after + // it does its work. Particularly the `lockedCallback` and `"locked"` message. Instead + // lockedCallback should be deprecated and people should subscribe and react to `authStatusFor$` themselves. + const lockPromise = firstValueFrom( + this.authService.authStatusFor$(lockingUserId).pipe( + filter((authStatus) => authStatus === AuthenticationStatus.Locked), + timeout({ + first: 5_000, + with: () => { + throw new Error("The lock process did not complete in a reasonable amount of time."); + }, + }), + ), + ); if (userId == null || userId === currentUserId) { await this.searchService.clearIndex(); @@ -96,20 +122,21 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { await this.collectionService.clearActiveUserCache(); } - await this.masterPasswordService.clearMasterKey((userId ?? currentUserId) as UserId); + await this.masterPasswordService.clearMasterKey(lockingUserId); - await this.stateService.setEverBeenUnlocked(true, { userId: userId }); - await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); - await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); + await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId }); + await this.stateService.setCryptoMasterKeyAuto(null, { userId: lockingUserId }); - await this.cipherService.clearCache(userId); + await this.cipherService.clearCache(lockingUserId); - await this.stateEventRunnerService.handleEvent("lock", (userId ?? currentUserId) as UserId); + await this.stateEventRunnerService.handleEvent("lock", lockingUserId); - // FIXME: We should send the userId of the user that was locked, in the case of this method being passed - // undefined then it should give back the currentUserId. Better yet, this method shouldn't take - // an undefined userId at all. All receivers need to be checked for how they handle getting undefined. - this.messagingService.send("locked", { userId: userId }); + // HACK: Sit here and wait for the the auth status to transition to `Locked` + // to ensure the message and lockedCallback will get the correct status + // if/when they call it. + await lockPromise; + + this.messagingService.send("locked", { userId: lockingUserId }); if (this.lockedCallback != null) { await this.lockedCallback(userId); @@ -124,6 +151,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { private async shouldLock( userId: string, + lastActive: Date, activeUserId: string, isViewOpen: boolean, ): Promise { @@ -147,17 +175,16 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { return false; } - const lastActive = await this.stateService.getLastActive({ userId: userId }); if (lastActive == null) { return false; } const vaultTimeoutSeconds = vaultTimeout * 60; - const diffSeconds = (new Date().getTime() - lastActive) / 1000; + const diffSeconds = (new Date().getTime() - lastActive.getTime()) / 1000; return diffSeconds >= vaultTimeoutSeconds; } - private async executeTimeoutAction(userId: string): Promise { + private async executeTimeoutAction(userId: UserId): Promise { const timeoutAction = await firstValueFrom( this.vaultTimeoutSettingsService.vaultTimeoutAction$(userId), ); @@ -165,21 +192,4 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { ? await this.logOut(userId) : await this.lock(userId); } - - private async migrateKeyForNeverLockIfNeeded(): Promise { - // Web can't set vault timeout to never - if (this.platformUtilsService.getClientType() == ClientType.Web) { - return; - } - const accounts = await firstValueFrom(this.stateService.accounts$); - for (const userId in accounts) { - if (userId != null) { - await this.cryptoService.migrateAutoKeyIfNeeded(userId); - // Legacy users should be logged out since we're not on the web vault and can't migrate. - if (await this.cryptoService.isLegacyUser(null, userId)) { - await this.logOut(userId); - } - } - } - } } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index f9a8734731..0a1f4b1d11 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -49,20 +49,22 @@ import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org- import { KeyConnectorMigrator } from "./migrations/50-move-key-connector-to-state-provider"; import { RememberedEmailMigrator } from "./migrations/51-move-remembered-email-to-state-providers"; import { DeleteInstalledVersion } from "./migrations/52-delete-installed-version"; -import { DeviceTrustCryptoServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-crypto-svc-to-state-providers"; +import { DeviceTrustServiceStateProviderMigrator } from "./migrations/53-migrate-device-trust-svc-to-state-providers"; import { SendMigrator } from "./migrations/54-move-encrypted-sends"; import { MoveMasterKeyStateToProviderMigrator } from "./migrations/55-move-master-key-state-to-provider"; import { AuthRequestMigrator } from "./migrations/56-move-auth-requests"; import { CipherServiceMigrator } from "./migrations/57-move-cipher-service-to-state-provider"; import { RemoveRefreshTokenMigratedFlagMigrator } from "./migrations/58-remove-refresh-token-migrated-state-provider-flag"; +import { KdfConfigMigrator } from "./migrations/59-move-kdf-config-to-state-provider"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; +import { KnownAccountsMigrator } from "./migrations/60-known-accounts"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; import { MoveStateVersionMigrator } from "./migrations/8-move-state-version"; import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global"; import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 58; +export const CURRENT_VERSION = 60; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -117,12 +119,14 @@ export function createMigrationBuilder() { .with(KeyConnectorMigrator, 49, 50) .with(RememberedEmailMigrator, 50, 51) .with(DeleteInstalledVersion, 51, 52) - .with(DeviceTrustCryptoServiceStateProviderMigrator, 52, 53) + .with(DeviceTrustServiceStateProviderMigrator, 52, 53) .with(SendMigrator, 53, 54) .with(MoveMasterKeyStateToProviderMigrator, 54, 55) .with(AuthRequestMigrator, 55, 56) .with(CipherServiceMigrator, 56, 57) - .with(RemoveRefreshTokenMigratedFlagMigrator, 57, CURRENT_VERSION); + .with(RemoveRefreshTokenMigratedFlagMigrator, 57, 58) + .with(KdfConfigMigrator, 58, 59) + .with(KnownAccountsMigrator, 59, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migration-builder.spec.ts b/libs/common/src/state-migrations/migration-builder.spec.ts index 6a4ff8e6d4..59d85609e0 100644 --- a/libs/common/src/state-migrations/migration-builder.spec.ts +++ b/libs/common/src/state-migrations/migration-builder.spec.ts @@ -1,5 +1,8 @@ import { mock } from "jest-mock-extended"; +// eslint-disable-next-line import/no-restricted-paths +import { ClientType } from "../enums"; + import { MigrationBuilder } from "./migration-builder"; import { MigrationHelper } from "./migration-helper"; import { Migrator } from "./migrator"; @@ -72,65 +75,69 @@ describe("MigrationBuilder", () => { expect(migrations[1]).toMatchObject({ migrator: expect.any(TestMigrator), direction: "down" }); }); - describe("migrate", () => { - let migrator: TestMigrator; - let rollback_migrator: TestMigrator; + const clientTypes = Object.values(ClientType); - beforeEach(() => { - sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); - migrator = (sut as any).migrations[0].migrator; - rollback_migrator = (sut as any).migrations[1].migrator; + describe.each(clientTypes)("for client %s", (clientType) => { + describe("migrate", () => { + let migrator: TestMigrator; + let rollback_migrator: TestMigrator; + + beforeEach(() => { + sut = sut.with(TestMigrator, 0, 1).rollback(TestMigrator, 1, 0); + migrator = (sut as any).migrations[0].migrator; + rollback_migrator = (sut as any).migrations[1].migrator; + }); + + it("should migrate", async () => { + const helper = new MigrationHelper(0, mock(), mock(), "general", clientType); + const spy = jest.spyOn(migrator, "migrate"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper); + }); + + it("should rollback", async () => { + const helper = new MigrationHelper(1, mock(), mock(), "general", clientType); + const spy = jest.spyOn(rollback_migrator, "rollback"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper); + }); + + it("should update version on migrate", async () => { + const helper = new MigrationHelper(0, mock(), mock(), "general", clientType); + const spy = jest.spyOn(migrator, "updateVersion"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper, "up"); + }); + + it("should update version on rollback", async () => { + const helper = new MigrationHelper(1, mock(), mock(), "general", clientType); + const spy = jest.spyOn(rollback_migrator, "updateVersion"); + await sut.migrate(helper); + expect(spy).toBeCalledWith(helper, "down"); + }); + + it("should not run the migrator if the current version does not match the from version", async () => { + const helper = new MigrationHelper(3, mock(), mock(), "general", clientType); + const migrate = jest.spyOn(migrator, "migrate"); + const rollback = jest.spyOn(rollback_migrator, "rollback"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); + + it("should not update version if the current version does not match the from version", async () => { + const helper = new MigrationHelper(3, mock(), mock(), "general", clientType); + const migrate = jest.spyOn(migrator, "updateVersion"); + const rollback = jest.spyOn(rollback_migrator, "updateVersion"); + await sut.migrate(helper); + expect(migrate).not.toBeCalled(); + expect(rollback).not.toBeCalled(); + }); }); - it("should migrate", async () => { - const helper = new MigrationHelper(0, mock(), mock(), "general"); - const spy = jest.spyOn(migrator, "migrate"); - await sut.migrate(helper); - expect(spy).toBeCalledWith(helper); + it("should be able to call instance methods", async () => { + const helper = new MigrationHelper(0, mock(), mock(), "general", clientType); + await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper); }); - - it("should rollback", async () => { - const helper = new MigrationHelper(1, mock(), mock(), "general"); - const spy = jest.spyOn(rollback_migrator, "rollback"); - await sut.migrate(helper); - expect(spy).toBeCalledWith(helper); - }); - - it("should update version on migrate", async () => { - const helper = new MigrationHelper(0, mock(), mock(), "general"); - const spy = jest.spyOn(migrator, "updateVersion"); - await sut.migrate(helper); - expect(spy).toBeCalledWith(helper, "up"); - }); - - it("should update version on rollback", async () => { - const helper = new MigrationHelper(1, mock(), mock(), "general"); - const spy = jest.spyOn(rollback_migrator, "updateVersion"); - await sut.migrate(helper); - expect(spy).toBeCalledWith(helper, "down"); - }); - - it("should not run the migrator if the current version does not match the from version", async () => { - const helper = new MigrationHelper(3, mock(), mock(), "general"); - const migrate = jest.spyOn(migrator, "migrate"); - const rollback = jest.spyOn(rollback_migrator, "rollback"); - await sut.migrate(helper); - expect(migrate).not.toBeCalled(); - expect(rollback).not.toBeCalled(); - }); - - it("should not update version if the current version does not match the from version", async () => { - const helper = new MigrationHelper(3, mock(), mock(), "general"); - const migrate = jest.spyOn(migrator, "updateVersion"); - const rollback = jest.spyOn(rollback_migrator, "updateVersion"); - await sut.migrate(helper); - expect(migrate).not.toBeCalled(); - expect(rollback).not.toBeCalled(); - }); - }); - - it("should be able to call instance methods", async () => { - const helper = new MigrationHelper(0, mock(), mock(), "general"); - await sut.with(TestMigratorWithInstanceMethod, 0, 1).migrate(helper); }); }); diff --git a/libs/common/src/state-migrations/migration-helper.spec.ts b/libs/common/src/state-migrations/migration-helper.spec.ts index f86cac8768..21c5c72a18 100644 --- a/libs/common/src/state-migrations/migration-helper.spec.ts +++ b/libs/common/src/state-migrations/migration-helper.spec.ts @@ -2,6 +2,8 @@ import { MockProxy, mock } from "jest-mock-extended"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { FakeStorageService } from "../../spec/fake-storage.service"; +// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -25,6 +27,14 @@ const exampleJSON = { }, global_serviceName_key: "global_serviceName_key", user_userId_serviceName_key: "user_userId_serviceName_key", + global_account_accounts: { + "c493ed01-4e08-4e88-abc7-332f380ca760": { + otherStuff: "otherStuff3", + }, + "23e61a5f-2ece-4f5e-b499-f0bc489482a9": { + otherStuff: "otherStuff4", + }, + }, }; describe("RemoveLegacyEtmKeyMigrator", () => { @@ -32,116 +42,164 @@ describe("RemoveLegacyEtmKeyMigrator", () => { let logService: MockProxy; let sut: MigrationHelper; - beforeEach(() => { - logService = mock(); - storage = mock(); - storage.get.mockImplementation((key) => (exampleJSON as any)[key]); + const clientTypes = Object.values(ClientType); - sut = new MigrationHelper(0, storage, logService, "general"); - }); + describe.each(clientTypes)("for client %s", (clientType) => { + beforeEach(() => { + logService = mock(); + storage = mock(); + storage.get.mockImplementation((key) => (exampleJSON as any)[key]); - describe("get", () => { - it("should delegate to storage.get", async () => { - await sut.get("key"); - expect(storage.get).toHaveBeenCalledWith("key"); - }); - }); - - describe("set", () => { - it("should delegate to storage.save", async () => { - await sut.set("key", "value"); - expect(storage.save).toHaveBeenCalledWith("key", "value"); - }); - }); - - describe("getAccounts", () => { - it("should return all accounts", async () => { - const accounts = await sut.getAccounts(); - expect(accounts).toEqual([ - { userId: "c493ed01-4e08-4e88-abc7-332f380ca760", account: { otherStuff: "otherStuff1" } }, - { userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", account: { otherStuff: "otherStuff2" } }, - ]); + sut = new MigrationHelper(0, storage, logService, "general", clientType); }); - it("should handle missing authenticatedAccounts", async () => { - storage.get.mockImplementation((key) => - key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key], - ); - const accounts = await sut.getAccounts(); - expect(accounts).toEqual([]); - }); - }); - - describe("getFromGlobal", () => { - it("should return the correct value", async () => { - sut.currentVersion = 9; - const value = await sut.getFromGlobal({ - stateDefinition: { name: "serviceName" }, - key: "key", + describe("get", () => { + it("should delegate to storage.get", async () => { + await sut.get("key"); + expect(storage.get).toHaveBeenCalledWith("key"); }); - expect(value).toEqual("global_serviceName_key"); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); - - describe("setToGlobal", () => { - it("should set the correct value", async () => { - sut.currentVersion = 9; - await sut.setToGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }, "new_value"); - expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value"); + describe("set", () => { + it("should delegate to storage.save", async () => { + await sut.set("key", "value"); + expect(storage.save).toHaveBeenCalledWith("key", "value"); + }); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.setToGlobal( + describe("getAccounts", () => { + it("should return all accounts", async () => { + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + { + userId: "c493ed01-4e08-4e88-abc7-332f380ca760", + account: { otherStuff: "otherStuff1" }, + }, + { + userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + account: { otherStuff: "otherStuff2" }, + }, + ]); + }); + + it("should handle missing authenticatedAccounts", async () => { + storage.get.mockImplementation((key) => + key === "authenticatedAccounts" ? undefined : (exampleJSON as any)[key], + ); + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([]); + }); + + it("handles global scoped known accounts for version 60 and after", async () => { + sut.currentVersion = 60; + const accounts = await sut.getAccounts(); + expect(accounts).toEqual([ + // Note, still gets values stored in state service objects, just grabs user ids from global + { + userId: "c493ed01-4e08-4e88-abc7-332f380ca760", + account: { otherStuff: "otherStuff1" }, + }, + { + userId: "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + account: { otherStuff: "otherStuff2" }, + }, + ]); + }); + }); + + describe("getKnownUserIds", () => { + it("returns all user ids", async () => { + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); + + it("returns all user ids when version is 60 or greater", async () => { + sut.currentVersion = 60; + const userIds = await sut.getKnownUserIds(); + expect(userIds).toEqual([ + "c493ed01-4e08-4e88-abc7-332f380ca760", + "23e61a5f-2ece-4f5e-b499-f0bc489482a9", + ]); + }); + }); + + describe("getFromGlobal", () => { + it("should return the correct value", async () => { + sut.currentVersion = 9; + const value = await sut.getFromGlobal({ + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("global_serviceName_key"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.getFromGlobal({ stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); + }); + + describe("setToGlobal", () => { + it("should set the correct value", async () => { + sut.currentVersion = 9; + await sut.setToGlobal( { stateDefinition: { name: "serviceName" }, key: "key" }, - "global_serviceName_key", - ), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); - - describe("getFromUser", () => { - it("should return the correct value", async () => { - sut.currentVersion = 9; - const value = await sut.getFromUser("userId", { - stateDefinition: { name: "serviceName" }, - key: "key", + "new_value", + ); + expect(storage.save).toHaveBeenCalledWith("global_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.setToGlobal( + { stateDefinition: { name: "serviceName" }, key: "key" }, + "global_serviceName_key", + ), + ).toThrowError("No key builder should be used for versions prior to 9."); }); - expect(value).toEqual("user_userId_serviceName_key"); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }), - ).toThrowError("No key builder should be used for versions prior to 9."); - }); - }); + describe("getFromUser", () => { + it("should return the correct value", async () => { + sut.currentVersion = 9; + const value = await sut.getFromUser("userId", { + stateDefinition: { name: "serviceName" }, + key: "key", + }); + expect(value).toEqual("user_userId_serviceName_key"); + }); - describe("setToUser", () => { - it("should set the correct value", async () => { - sut.currentVersion = 9; - await sut.setToUser( - "userId", - { stateDefinition: { name: "serviceName" }, key: "key" }, - "new_value", - ); - expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value"); + it("should throw if the current version is less than 9", () => { + expect(() => + sut.getFromUser("userId", { stateDefinition: { name: "serviceName" }, key: "key" }), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); }); - it("should throw if the current version is less than 9", () => { - expect(() => - sut.setToUser( + describe("setToUser", () => { + it("should set the correct value", async () => { + sut.currentVersion = 9; + await sut.setToUser( "userId", { stateDefinition: { name: "serviceName" }, key: "key" }, "new_value", - ), - ).toThrowError("No key builder should be used for versions prior to 9."); + ); + expect(storage.save).toHaveBeenCalledWith("user_userId_serviceName_key", "new_value"); + }); + + it("should throw if the current version is less than 9", () => { + expect(() => + sut.setToUser( + "userId", + { stateDefinition: { name: "serviceName" }, key: "key" }, + "new_value", + ), + ).toThrowError("No key builder should be used for versions prior to 9."); + }); }); }); }); @@ -151,6 +209,7 @@ export function mockMigrationHelper( storageJson: any, stateVersion = 0, type: MigrationHelperType = "general", + clientType: ClientType = ClientType.Web, ): MockProxy { const logService: MockProxy = mock(); const storage: MockProxy = mock(); @@ -158,7 +217,7 @@ export function mockMigrationHelper( storage.save.mockImplementation(async (key, value) => { (storageJson as any)[key] = value; }); - const helper = new MigrationHelper(stateVersion, storage, logService, type); + const helper = new MigrationHelper(stateVersion, storage, logService, type, clientType); const mockHelper = mock(); mockHelper.get.mockImplementation((key) => helper.get(key)); @@ -176,6 +235,11 @@ export function mockMigrationHelper( helper.setToUser(userId, keyDefinition, value), ); mockHelper.getAccounts.mockImplementation(() => helper.getAccounts()); + mockHelper.getKnownUserIds.mockImplementation(() => helper.getKnownUserIds()); + mockHelper.removeFromGlobal.mockImplementation((keyDefinition) => + helper.removeFromGlobal(keyDefinition), + ); + mockHelper.remove.mockImplementation((key) => helper.remove(key)); mockHelper.type = helper.type; @@ -295,7 +359,13 @@ export async function runMigrator< const allInjectedData = injectData(initalData, []); const fakeStorageService = new FakeStorageService(initalData); - const helper = new MigrationHelper(migrator.fromVersion, fakeStorageService, mock(), "general"); + const helper = new MigrationHelper( + migrator.fromVersion, + fakeStorageService, + mock(), + "general", + ClientType.Web, + ); // Run their migrations if (direction === "rollback") { diff --git a/libs/common/src/state-migrations/migration-helper.ts b/libs/common/src/state-migrations/migration-helper.ts index 5b8e4ff93e..b377df8ef9 100644 --- a/libs/common/src/state-migrations/migration-helper.ts +++ b/libs/common/src/state-migrations/migration-helper.ts @@ -1,3 +1,5 @@ +// eslint-disable-next-line import/no-restricted-paths -- Needed to provide client type to migrations +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -17,6 +19,7 @@ export class MigrationHelper { private storageService: AbstractStorageService, public logService: LogService, type: MigrationHelperType, + public clientType: ClientType, ) { this.type = type; } @@ -154,12 +157,12 @@ export class MigrationHelper { * * This is useful from creating migrations off of this paradigm, but should not be used once a value is migrated to a state provider. * - * @returns a list of all accounts that have been authenticated with state service, cast the the expected type. + * @returns a list of all accounts that have been authenticated with state service, cast the expected type. */ async getAccounts(): Promise< { userId: string; account: ExpectedAccountType }[] > { - const userIds = (await this.get("authenticatedAccounts")) ?? []; + const userIds = await this.getKnownUserIds(); return Promise.all( userIds.map(async (userId) => ({ userId, @@ -168,6 +171,17 @@ export class MigrationHelper { ); } + /** + * Helper method to read known users ids. + */ + async getKnownUserIds(): Promise { + if (this.currentVersion < 60) { + return knownAccountUserIdsBuilderPre60(this.storageService); + } else { + return knownAccountUserIdsBuilder(this.storageService); + } + } + /** * Builds a user storage key appropriate for the current version. * @@ -230,3 +244,18 @@ function globalKeyBuilder(keyDefinition: KeyDefinitionLike): string { function globalKeyBuilderPre9(): string { throw Error("No key builder should be used for versions prior to 9."); } + +async function knownAccountUserIdsBuilderPre60( + storageService: AbstractStorageService, +): Promise { + return (await storageService.get("authenticatedAccounts")) ?? []; +} + +async function knownAccountUserIdsBuilder( + storageService: AbstractStorageService, +): Promise { + const accounts = await storageService.get>( + globalKeyBuilder({ stateDefinition: { name: "account" }, key: "accounts" }), + ); + return Object.keys(accounts ?? {}); +} diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts similarity index 92% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts index 79366a4716..343fbd03d9 100644 --- a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.spec.ts +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.spec.ts @@ -5,9 +5,9 @@ import { mockMigrationHelper } from "../migration-helper.spec"; import { DEVICE_KEY, - DeviceTrustCryptoServiceStateProviderMigrator, + DeviceTrustServiceStateProviderMigrator, SHOULD_TRUST_DEVICE, -} from "./53-migrate-device-trust-crypto-svc-to-state-providers"; +} from "./53-migrate-device-trust-svc-to-state-providers"; // Represents data in state service pre-migration function preMigrationJson() { @@ -79,14 +79,14 @@ function rollbackJSON() { }; } -describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { +describe("DeviceTrustServiceStateProviderMigrator", () => { let helper: MockProxy; - let sut: DeviceTrustCryptoServiceStateProviderMigrator; + let sut: DeviceTrustServiceStateProviderMigrator; describe("migrate", () => { beforeEach(() => { helper = mockMigrationHelper(preMigrationJson(), 52); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); // it should remove deviceKey and trustDeviceChoiceForDecryption from all accounts @@ -126,7 +126,7 @@ describe("DeviceTrustCryptoServiceStateProviderMigrator", () => { describe("rollback", () => { beforeEach(() => { helper = mockMigrationHelper(rollbackJSON(), 53); - sut = new DeviceTrustCryptoServiceStateProviderMigrator(52, 53); + sut = new DeviceTrustServiceStateProviderMigrator(52, 53); }); it("should null out newly migrated entries in state provider framework", async () => { diff --git a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts similarity index 94% rename from libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts rename to libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts index e19c7b3fa5..b6d2c19b15 100644 --- a/libs/common/src/state-migrations/migrations/53-migrate-device-trust-crypto-svc-to-state-providers.ts +++ b/libs/common/src/state-migrations/migrations/53-migrate-device-trust-svc-to-state-providers.ts @@ -16,7 +16,7 @@ type ExpectedAccountType = { }; export const DEVICE_KEY: KeyDefinitionLike = { - key: "deviceKey", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "deviceKey", // matches KeyDefinition.key in DeviceTrustService stateDefinition: { name: "deviceTrust", // matches StateDefinition.name in StateDefinitions }, @@ -29,7 +29,7 @@ export const SHOULD_TRUST_DEVICE: KeyDefinitionLike = { }, }; -export class DeviceTrustCryptoServiceStateProviderMigrator extends Migrator<52, 53> { +export class DeviceTrustServiceStateProviderMigrator extends Migrator<52, 53> { async migrate(helper: MigrationHelper): Promise { const accounts = await helper.getAccounts(); async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts index 499cff1c89..f51699bc79 100644 --- a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.spec.ts @@ -26,11 +26,13 @@ function exampleJSON() { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, @@ -150,11 +152,13 @@ describe("CipherServiceMigrator", () => { }, }, ciphers: { - "cipher-id-10": { - id: "cipher-id-10", - }, - "cipher-id-11": { - id: "cipher-id-11", + encrypted: { + "cipher-id-10": { + id: "cipher-id-10", + }, + "cipher-id-11": { + id: "cipher-id-11", + }, }, }, }, diff --git a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts index e71d889bb7..80c776e1b6 100644 --- a/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/57-move-cipher-service-to-state-provider.ts @@ -4,7 +4,9 @@ import { Migrator } from "../migrator"; type ExpectedAccountType = { data: { localData?: unknown; - ciphers?: unknown; + ciphers?: { + encrypted: unknown; + }; }; }; @@ -37,7 +39,7 @@ export class CipherServiceMigrator extends Migrator<56, 57> { } //Migrate ciphers - const ciphers = account?.data?.ciphers; + const ciphers = account?.data?.ciphers?.encrypted; if (ciphers != null) { await helper.setToUser(userId, CIPHERS_DISK, ciphers); delete account.data.ciphers; @@ -68,7 +70,8 @@ export class CipherServiceMigrator extends Migrator<56, 57> { const ciphers = await helper.getFromUser(userId, CIPHERS_DISK); if (account.data && ciphers != null) { - account.data.ciphers = ciphers; + account.data.ciphers ||= { encrypted: null }; + account.data.ciphers.encrypted = ciphers; await helper.set(userId, account); } await helper.setToUser(userId, CIPHERS_DISK, null); diff --git a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts index 9c6d3776fe..1fb3609267 100644 --- a/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts +++ b/libs/common/src/state-migrations/migrations/58-remove-refresh-token-migrated-state-provider-flag.ts @@ -4,7 +4,7 @@ import { IRREVERSIBLE, Migrator } from "../migrator"; type ExpectedAccountType = NonNullable; export const REFRESH_TOKEN_MIGRATED_TO_SECURE_STORAGE: KeyDefinitionLike = { - key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key in DeviceTrustCryptoService + key: "refreshTokenMigratedToSecureStorage", // matches KeyDefinition.key stateDefinition: { name: "token", // matches StateDefinition.name in StateDefinitions }, diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts new file mode 100644 index 0000000000..dbce750a7e --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.spec.ts @@ -0,0 +1,153 @@ +import { MockProxy } from "jest-mock-extended"; + +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { KdfConfigMigrator } from "./59-move-kdf-config-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }, + SecondAccount: { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_kdfConfig_kdfConfig: { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }, + user_SecondAccount_kdfConfig_kdfConfig: { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount"], + FirstAccount: { + profile: { + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +describe("KdfConfigMigrator", () => { + let helper: MockProxy; + let sut: KdfConfigMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should remove kdfType and kdfConfig from Account.Profile", async () => { + await sut.migrate(helper); + + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "otherStuff1", + }, + otherStuff: "otherStuff2", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff3", + }, + otherStuff: "otherStuff4", + }); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, { + iterations: 3, + memory: 64, + parallelism: 5, + kdfType: 1, + }); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, { + iterations: 600_001, + memory: null as number, + parallelism: null as number, + kdfType: 0, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 59); + sut = new KdfConfigMigrator(58, 59); + }); + + it("should null out new KdfConfig account value and set account.profile", async () => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledTimes(2); + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", kdfConfigKeyDefinition, null); + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", kdfConfigKeyDefinition, null); + expect(helper.set).toHaveBeenCalledTimes(2); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + kdfIterations: 3, + kdfMemory: 64, + kdfParallelism: 5, + kdfType: 1, + otherStuff: "otherStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + kdfIterations: 600_001, + kdfMemory: null as number, + kdfParallelism: null as number, + kdfType: 0, + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts new file mode 100644 index 0000000000..332306c6d4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/59-move-kdf-config-to-state-provider.ts @@ -0,0 +1,78 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +enum KdfType { + PBKDF2_SHA256 = 0, + Argon2id = 1, +} + +class KdfConfig { + iterations: number; + kdfType: KdfType; + memory?: number; + parallelism?: number; +} + +type ExpectedAccountType = { + profile?: { + kdfIterations: number; + kdfType: KdfType; + kdfMemory?: number; + kdfParallelism?: number; + }; +}; + +const kdfConfigKeyDefinition: KeyDefinitionLike = { + key: "kdfConfig", + stateDefinition: { + name: "kdfConfig", + }, +}; + +export class KdfConfigMigrator extends Migrator<58, 59> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const iterations = account?.profile?.kdfIterations; + const kdfType = account?.profile?.kdfType; + const memory = account?.profile?.kdfMemory; + const parallelism = account?.profile?.kdfParallelism; + + const kdfConfig: KdfConfig = { + iterations: iterations, + kdfType: kdfType, + memory: memory, + parallelism: parallelism, + }; + + if (kdfConfig != null) { + await helper.setToUser(userId, kdfConfigKeyDefinition, kdfConfig); + delete account?.profile?.kdfIterations; + delete account?.profile?.kdfType; + delete account?.profile?.kdfMemory; + delete account?.profile?.kdfParallelism; + } + + await helper.set(userId, account); + } + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const kdfConfig: KdfConfig = await helper.getFromUser(userId, kdfConfigKeyDefinition); + + if (kdfConfig != null) { + account.profile.kdfIterations = kdfConfig.iterations; + account.profile.kdfType = kdfConfig.kdfType; + account.profile.kdfMemory = kdfConfig.memory; + account.profile.kdfParallelism = kdfConfig.parallelism; + await helper.setToUser(userId, kdfConfigKeyDefinition, null); + } + await helper.set(userId, account); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts new file mode 100644 index 0000000000..01be4adb6a --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.spec.ts @@ -0,0 +1,141 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { + ACCOUNT_ACCOUNTS, + ACCOUNT_ACTIVE_ACCOUNT_ID, + ACCOUNT_ACTIVITY, + KnownAccountsMigrator, +} from "./60-known-accounts"; + +const migrateJson = () => { + return { + authenticatedAccounts: ["user1", "user2"], + activeUserId: "user1", + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + accountActivity: { + user1: 1609459200000, // 2021-01-01 + user2: 1609545600000, // 2021-01-02 + }, + }; +}; + +const rollbackJson = () => { + return { + user1: { + profile: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + }, + user2: { + profile: { + email: "", + emailVerified: false, + }, + }, + global_account_accounts: { + user1: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "", + emailVerified: false, + }, + }, + global_account_activeAccountId: "user1", + global_account_activity: { + user1: "2021-01-01T00:00:00.000Z", + user2: "2021-01-02T00:00:00.000Z", + }, + }; +}; + +describe("ReplicateKnownAccounts", () => { + let helper: MockProxy; + let sut: KnownAccountsMigrator; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(migrateJson(), 59); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("migrates accounts", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS, { + user1: { + email: "user1", + name: "User 1", + emailVerified: true, + }, + user2: { + email: "", + emailVerified: false, + name: undefined, + }, + }); + expect(helper.remove).toHaveBeenCalledWith("authenticatedAccounts"); + }); + + it("migrates active account it", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID, "user1"); + expect(helper.remove).toHaveBeenCalledWith("activeUserId"); + }); + + it("migrates account activity", async () => { + await sut.migrate(helper); + expect(helper.setToGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY, { + user1: '"2021-01-01T00:00:00.000Z"', + user2: '"2021-01-02T00:00:00.000Z"', + }); + expect(helper.remove).toHaveBeenCalledWith("accountActivity"); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJson(), 60); + sut = new KnownAccountsMigrator(59, 60); + }); + + it("rolls back authenticated accounts", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("authenticatedAccounts", ["user1", "user2"]); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACCOUNTS); + }); + + it("rolls back active account id", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("activeUserId", "user1"); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVE_ACCOUNT_ID); + }); + + it("rolls back account activity", async () => { + await sut.rollback(helper); + expect(helper.set).toHaveBeenCalledWith("accountActivity", { + user1: 1609459200000, + user2: 1609545600000, + }); + expect(helper.removeFromGlobal).toHaveBeenCalledWith(ACCOUNT_ACTIVITY); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/60-known-accounts.ts b/libs/common/src/state-migrations/migrations/60-known-accounts.ts new file mode 100644 index 0000000000..3b02a5acc4 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/60-known-accounts.ts @@ -0,0 +1,111 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +export const ACCOUNT_ACCOUNTS: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "accounts", +}; + +export const ACCOUNT_ACTIVE_ACCOUNT_ID: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activeAccountId", +}; + +export const ACCOUNT_ACTIVITY: KeyDefinitionLike = { + stateDefinition: { + name: "account", + }, + key: "activity", +}; + +type ExpectedAccountType = { + profile?: { + email?: string; + name?: string; + emailVerified?: boolean; + }; +}; + +export class KnownAccountsMigrator extends Migrator<59, 60> { + async migrate(helper: MigrationHelper): Promise { + await this.migrateAuthenticatedAccounts(helper); + await this.migrateActiveAccountId(helper); + await this.migrateAccountActivity(helper); + } + async rollback(helper: MigrationHelper): Promise { + // authenticated account are removed, but the accounts record also contains logged out accounts. Best we can do is to add them all back + const userIds = (await helper.getKnownUserIds()) ?? []; + await helper.set("authenticatedAccounts", userIds); + await helper.removeFromGlobal(ACCOUNT_ACCOUNTS); + + // Active Account Id + const activeAccountId = await helper.getFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID); + if (activeAccountId) { + await helper.set("activeUserId", activeAccountId); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID); + + // Account Activity + const accountActivity = await helper.getFromGlobal>(ACCOUNT_ACTIVITY); + if (accountActivity) { + const toStore = Object.entries(accountActivity).reduce( + (agg, [userId, dateString]) => { + agg[userId] = new Date(dateString).getTime(); + return agg; + }, + {} as Record, + ); + await helper.set("accountActivity", toStore); + } + await helper.removeFromGlobal(ACCOUNT_ACTIVITY); + } + + private async migrateAuthenticatedAccounts(helper: MigrationHelper) { + const authenticatedAccounts = (await helper.get("authenticatedAccounts")) ?? []; + const accounts = await Promise.all( + authenticatedAccounts.map(async (userId) => { + const account = await helper.get(userId); + return { userId, account }; + }), + ); + const accountsToStore = accounts.reduce( + (agg, { userId, account }) => { + if (account?.profile) { + agg[userId] = { + email: account.profile.email ?? "", + emailVerified: account.profile.emailVerified ?? false, + name: account.profile.name, + }; + } + return agg; + }, + {} as Record, + ); + + await helper.setToGlobal(ACCOUNT_ACCOUNTS, accountsToStore); + await helper.remove("authenticatedAccounts"); + } + + private async migrateAccountActivity(helper: MigrationHelper) { + const stored = await helper.get>("accountActivity"); + const accountActivity = Object.entries(stored ?? {}).reduce( + (agg, [userId, dateMs]) => { + agg[userId] = JSON.stringify(new Date(dateMs)); + return agg; + }, + {} as Record, + ); + await helper.setToGlobal(ACCOUNT_ACTIVITY, accountActivity); + await helper.remove("accountActivity"); + } + + private async migrateActiveAccountId(helper: MigrationHelper) { + const activeAccountId = await helper.get("activeUserId"); + await helper.setToGlobal(ACCOUNT_ACTIVE_ACCOUNT_ID, activeAccountId); + await helper.remove("activeUserId"); + } +} diff --git a/libs/common/src/state-migrations/migrator.spec.ts b/libs/common/src/state-migrations/migrator.spec.ts index d1189c25ea..4079dc3fda 100644 --- a/libs/common/src/state-migrations/migrator.spec.ts +++ b/libs/common/src/state-migrations/migrator.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +// eslint-disable-next-line import/no-restricted-paths -- Needed client type enum +import { ClientType } from "../enums"; // eslint-disable-next-line import/no-restricted-paths -- Needed to print log messages import { LogService } from "../platform/abstractions/log.service"; // eslint-disable-next-line import/no-restricted-paths -- Needed to interface with storage locations @@ -23,52 +25,56 @@ describe("migrator default methods", () => { let helper: MigrationHelper; let sut: TestMigrator; - beforeEach(() => { - storage = mock(); - logService = mock(); - helper = new MigrationHelper(0, storage, logService, "general"); - sut = new TestMigrator(0, 1); - }); + const clientTypes = Object.values(ClientType); - describe("shouldMigrate", () => { - describe("up", () => { - it("should return true if the current version equals the from version", async () => { - expect(await sut.shouldMigrate(helper, "up")).toBe(true); + describe.each(clientTypes)("for client %s", (clientType) => { + beforeEach(() => { + storage = mock(); + logService = mock(); + helper = new MigrationHelper(0, storage, logService, "general", clientType); + sut = new TestMigrator(0, 1); + }); + + describe("shouldMigrate", () => { + describe("up", () => { + it("should return true if the current version equals the from version", async () => { + expect(await sut.shouldMigrate(helper, "up")).toBe(true); + }); + + it("should return false if the current version does not equal the from version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "up")).toBe(false); + }); }); - it("should return false if the current version does not equal the from version", async () => { - helper.currentVersion = 1; - expect(await sut.shouldMigrate(helper, "up")).toBe(false); + describe("down", () => { + it("should return true if the current version equals the to version", async () => { + helper.currentVersion = 1; + expect(await sut.shouldMigrate(helper, "down")).toBe(true); + }); + + it("should return false if the current version does not equal the to version", async () => { + expect(await sut.shouldMigrate(helper, "down")).toBe(false); + }); }); }); - describe("down", () => { - it("should return true if the current version equals the to version", async () => { - helper.currentVersion = 1; - expect(await sut.shouldMigrate(helper, "down")).toBe(true); + describe("updateVersion", () => { + describe("up", () => { + it("should update the version", async () => { + await sut.updateVersion(helper, "up"); + expect(storage.save).toBeCalledWith("stateVersion", 1); + expect(helper.currentVersion).toBe(1); + }); }); - it("should return false if the current version does not equal the to version", async () => { - expect(await sut.shouldMigrate(helper, "down")).toBe(false); - }); - }); - }); - - describe("updateVersion", () => { - describe("up", () => { - it("should update the version", async () => { - await sut.updateVersion(helper, "up"); - expect(storage.save).toBeCalledWith("stateVersion", 1); - expect(helper.currentVersion).toBe(1); - }); - }); - - describe("down", () => { - it("should update the version", async () => { - helper.currentVersion = 1; - await sut.updateVersion(helper, "down"); - expect(storage.save).toBeCalledWith("stateVersion", 0); - expect(helper.currentVersion).toBe(0); + describe("down", () => { + it("should update the version", async () => { + helper.currentVersion = 1; + await sut.updateVersion(helper, "down"); + expect(storage.save).toBeCalledWith("stateVersion", 0); + expect(helper.currentVersion).toBe(0); + }); }); }); }); diff --git a/libs/common/src/tools/generator/state/buffered-key-definition.ts b/libs/common/src/tools/generator/state/buffered-key-definition.ts index 5457410f80..1f11280839 100644 --- a/libs/common/src/tools/generator/state/buffered-key-definition.ts +++ b/libs/common/src/tools/generator/state/buffered-key-definition.ts @@ -87,9 +87,13 @@ export class BufferedKeyDefinition { } /** Checks whether the input type can be converted to the output type. - * @returns `true` if the definition is valid, otherwise `false`. + * @returns `true` if the definition is defined and valid, otherwise `false`. */ isValid(input: Input, dependency: Dependency) { + if (input === null) { + return Promise.resolve(false); + } + const isValid = this.options?.isValid; if (isValid) { return isValid(input, dependency); diff --git a/libs/common/src/tools/generator/state/buffered-state.spec.ts b/libs/common/src/tools/generator/state/buffered-state.spec.ts index 7f9722d384..46e132c1bd 100644 --- a/libs/common/src/tools/generator/state/buffered-state.spec.ts +++ b/libs/common/src/tools/generator/state/buffered-state.spec.ts @@ -75,14 +75,16 @@ describe("BufferedState", () => { it("rolls over pending values from the buffered state immediately by default", async () => { const provider = new FakeStateProvider(accountService); const outputState = provider.getUser(SomeUser, SOME_KEY); - await outputState.update(() => ({ foo: true, bar: false })); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); const bufferedState = new BufferedState(provider, BUFFER_KEY, outputState); const bufferedValue = { foo: true, bar: true }; await provider.setUserState(BUFFER_KEY.toKeyDefinition(), bufferedValue, SomeUser); - const result = await firstValueFrom(bufferedState.state$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); // also important for data migrations @@ -131,14 +133,16 @@ describe("BufferedState", () => { }); const provider = new FakeStateProvider(accountService); const outputState = provider.getUser(SomeUser, SOME_KEY); - await outputState.update(() => ({ foo: true, bar: false })); + const initialValue = { foo: true, bar: false }; + await outputState.update(() => initialValue); const bufferedState = new BufferedState(provider, bufferedKey, outputState); const bufferedValue = { foo: true, bar: true }; await provider.setUserState(bufferedKey.toKeyDefinition(), bufferedValue, SomeUser); - const result = await firstValueFrom(bufferedState.state$); + const result = await trackEmissions(bufferedState.state$); + await awaitAsync(); - expect(result).toEqual(bufferedValue); + expect(result).toEqual([initialValue, bufferedValue]); }); it("reads from the output state when shouldOverwrite returns a falsy value", async () => { @@ -274,7 +278,7 @@ describe("BufferedState", () => { await bufferedState.buffer(bufferedValue); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue]); + expect(result).toEqual([firstValue]); }); it("replaces the output state when its dependency becomes true", async () => { @@ -296,7 +300,7 @@ describe("BufferedState", () => { dependency.next(true); await awaitAsync(); - expect(result).toEqual([firstValue, firstValue, bufferedValue]); + expect(result).toEqual([firstValue, bufferedValue]); }); it.each([[null], [undefined]])("ignores `%p`", async (bufferedValue) => { @@ -325,11 +329,13 @@ describe("BufferedState", () => { await outputState.update(() => firstValue); const bufferedState = new BufferedState(provider, bufferedKey, outputState); - const result = trackEmissions(bufferedState.state$); + const stateResult = trackEmissions(bufferedState.state$); await bufferedState.buffer({ foo: true, bar: true }); await awaitAsync(); + const bufferedResult = await firstValueFrom(bufferedState.bufferedState$); - expect(result).toEqual([firstValue, firstValue]); + expect(stateResult).toEqual([firstValue]); + expect(bufferedResult).toBeNull(); }); it("overwrites the output when isValid returns true", async () => { diff --git a/libs/common/src/tools/generator/state/buffered-state.ts b/libs/common/src/tools/generator/state/buffered-state.ts index 42b14b815c..bb4de645e9 100644 --- a/libs/common/src/tools/generator/state/buffered-state.ts +++ b/libs/common/src/tools/generator/state/buffered-state.ts @@ -1,4 +1,4 @@ -import { Observable, combineLatest, concatMap, filter, map, of } from "rxjs"; +import { Observable, combineLatest, concatMap, filter, map, of, concat, merge } from "rxjs"; import { StateProvider, @@ -33,68 +33,53 @@ export class BufferedState implements SingleUserState private output: SingleUserState, dependency$: Observable = null, ) { - this.bufferState = provider.getUser(output.userId, key.toKeyDefinition()); + this.bufferedState = provider.getUser(output.userId, key.toKeyDefinition()); - const watching = [ - this.bufferState.state$, - this.output.state$, - dependency$ ?? of(true as unknown as Dependency), - ] as const; - - this.state$ = combineLatest(watching).pipe( - concatMap(async ([input, output, dependency]) => { - const normalized = input ?? null; - - const canOverwrite = normalized !== null && key.shouldOverwrite(dependency); - if (canOverwrite) { - await this.updateOutput(dependency); - - // prevent duplicate updates by suppressing the update - return [false, output] as const; + // overwrite the output value + const hasValue$ = concat(of(null), this.bufferedState.state$).pipe( + map((buffer) => (buffer ?? null) !== null), + ); + const overwriteDependency$ = (dependency$ ?? of(true as unknown as Dependency)).pipe( + map((dependency) => [key.shouldOverwrite(dependency), dependency] as const), + ); + const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe( + concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => { + if (hasValue && shouldOverwrite) { + await this.overwriteOutput(dependency); } - - return [true, output] as const; + return [false, null] as const; }), - filter(([updated]) => updated), + ); + + // drive overwrites only when there's a subscription; + // the output state determines when emissions occur + const output$ = this.output.state$.pipe(map((output) => [true, output] as const)); + this.state$ = merge(overwrite$, output$).pipe( + filter(([emit]) => emit), map(([, output]) => output), ); this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state])); - this.bufferState$ = this.bufferState.state$; + this.bufferedState$ = this.bufferedState.state$; } - private bufferState: SingleUserState; + private bufferedState: SingleUserState; - private async updateOutput(dependency: Dependency) { - // retrieve the latest input value - let input: Input; - await this.bufferState.update((state) => state, { - shouldUpdate: (state) => { - input = state; - return false; - }, + private async overwriteOutput(dependency: Dependency) { + // take the latest value from the buffer + let buffered: Input; + await this.bufferedState.update((state) => { + buffered = state ?? null; + return null; }); - // bail if this update lost the race with the last update - if (input === null) { - return; + // update the output state + const isValid = await this.key.isValid(buffered, dependency); + if (isValid) { + const output = await this.key.map(buffered, dependency); + await this.output.update(() => output); } - - // destroy invalid data and bail - if (!(await this.key.isValid(input, dependency))) { - await this.bufferState.update(() => null); - return; - } - - // overwrite anything left to the output; the updates need to be awaited with `Promise.all` - // so that `inputState.update(() => null)` runs before `shouldUpdate` reads the value (above). - // This lets the emission from `this.outputState.update` renter the `concatMap`. If the - // awaits run in sequence, it can win the race and cause a double emission. - const output = await this.key.map(input, dependency); - await Promise.all([this.output.update(() => output), this.bufferState.update(() => null)]); - - return; } /** {@link SingleUserState.userId} */ @@ -119,14 +104,14 @@ export class BufferedState implements SingleUserState async buffer(value: Input): Promise { const normalized = value ?? null; if (normalized !== null) { - await this.bufferState.update(() => normalized); + await this.bufferedState.update(() => normalized); } } /** The data presently being buffered. This emits the pending value each time * new buffer data is provided. It emits null when the buffer is empty. */ - readonly bufferState$: Observable; + readonly bufferedState$: Observable; /** Updates the output state. * @param configureState a callback that returns an updated output 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 41183c42af..2f0f50c616 100644 --- a/libs/common/src/tools/send/services/send.service.spec.ts +++ b/libs/common/src/tools/send/services/send.service.spec.ts @@ -62,6 +62,7 @@ describe("SendService", () => { accountService.activeAccountSubject.next({ id: mockUserId, email: "email", + emailVerified: false, name: "name", }); diff --git a/libs/common/src/tools/send/services/send.service.ts b/libs/common/src/tools/send/services/send.service.ts index 33b1f28be0..fb67de5501 100644 --- a/libs/common/src/tools/send/services/send.service.ts +++ b/libs/common/src/tools/send/services/send.service.ts @@ -1,10 +1,10 @@ import { Observable, concatMap, distinctUntilChanged, firstValueFrom, map } from "rxjs"; +import { PBKDF2KdfConfig } from "../../../auth/models/domain/kdf-config"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service"; -import { KdfType } from "../../../platform/enums"; import { Utils } from "../../../platform/misc/utils"; import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../../platform/models/domain/enc-string"; @@ -69,8 +69,7 @@ export class SendService implements InternalSendServiceAbstraction { const passwordKey = await this.keyGenerationService.deriveKeyFromPassword( password, model.key, - KdfType.PBKDF2_SHA256, - { iterations: SEND_KDF_ITERATIONS }, + new PBKDF2KdfConfig(SEND_KDF_ITERATIONS), ); send.password = passwordKey.keyB64; } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 501fd87665..22e2c54a59 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -47,8 +47,24 @@ export abstract class CipherService { updateLastUsedDate: (id: string) => Promise; updateLastLaunchedDate: (id: string) => Promise; saveNeverDomain: (domain: string) => Promise; - createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; - updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; + /** + * Create a cipher with the server + * + * @param cipher The cipher to create + * @param orgAdmin If true, the request is submitted as an organization admin request + * + * @returns A promise that resolves to the created cipher + */ + createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise; + /** + * Update a cipher with the server + * @param cipher The cipher to update + * @param orgAdmin If true, the request is submitted as an organization admin request + * @param isNotClone If true, the cipher is not a clone and should be treated as a new cipher + * + * @returns A promise that resolves to the updated cipher + */ + updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise; shareWithServer: ( cipher: CipherView, organizationId: string, @@ -70,7 +86,14 @@ export abstract class CipherService { data: ArrayBuffer, admin?: boolean, ) => Promise; - saveCollectionsWithServer: (cipher: Cipher) => Promise; + /** + * Save the collections for a cipher with the server + * + * @param cipher The cipher to save collections for + * + * @returns A promise that resolves when the collections have been saved + */ + saveCollectionsWithServer: (cipher: Cipher) => Promise; /** * Bulk update collections for many ciphers with the server * @param orgId @@ -84,7 +107,13 @@ export abstract class CipherService { collectionIds: CollectionId[], removeCollections: boolean, ) => Promise; - upsert: (cipher: CipherData | CipherData[]) => Promise; + /** + * Update the local store of CipherData with the provided data. Values are upserted into the existing store. + * + * @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects. + * @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated + */ + upsert: (cipher: CipherData | CipherData[]) => Promise>; replace: (ciphers: { [id: string]: CipherData }) => Promise; clear: (userId: string) => Promise; moveManyWithServer: (ids: string[], folderId: string) => Promise; diff --git a/libs/common/src/vault/models/domain/collection.spec.ts b/libs/common/src/vault/models/domain/collection.spec.ts index cd1cab8b42..4ee725be57 100644 --- a/libs/common/src/vault/models/domain/collection.spec.ts +++ b/libs/common/src/vault/models/domain/collection.spec.ts @@ -61,6 +61,7 @@ describe("Collection", () => { const view = await collection.decrypt(); expect(view).toEqual({ + addAccess: false, externalId: "extId", hidePasswords: false, id: "id", diff --git a/libs/common/src/vault/models/view/collection.view.ts b/libs/common/src/vault/models/view/collection.view.ts index 86766bdeac..ebc0229f4e 100644 --- a/libs/common/src/vault/models/view/collection.view.ts +++ b/libs/common/src/vault/models/view/collection.view.ts @@ -17,6 +17,7 @@ export class CollectionView implements View, ITreeNodeObject { readOnly: boolean = null; hidePasswords: boolean = null; manage: boolean = null; + addAccess: boolean = false; assigned: boolean = null; constructor(c?: Collection | CollectionAccessDetailsResponse) { @@ -38,7 +39,11 @@ export class CollectionView implements View, ITreeNodeObject { } } - canEditItems(org: Organization, v1FlexibleCollections: boolean): boolean { + canEditItems( + org: Organization, + v1FlexibleCollections: boolean, + restrictProviderAccess: boolean, + ): boolean { if (org != null && org.id !== this.organizationId) { throw new Error( "Id of the organization provided does not match the org id of the collection.", @@ -47,7 +52,7 @@ export class CollectionView implements View, ITreeNodeObject { if (org?.flexibleCollections) { return ( - org?.canEditAllCiphers(v1FlexibleCollections) || + org?.canEditAllCiphers(v1FlexibleCollections, restrictProviderAccess) || this.manage || (this.assigned && !this.readOnly) ); diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 28c4bfc653..9b03753118 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -174,23 +174,20 @@ describe("Cipher Service", () => { it("should call apiService.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => { const spy = jest .spyOn(apiService, "postCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith(expectedObj); }); + it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => { cipherObj.organizationId = null; const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -201,10 +198,8 @@ describe("Cipher Service", () => { cipherObj.collectionIds = ["123"]; const spy = jest .spyOn(apiService, "postCipherCreate") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherCreateRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -214,10 +209,8 @@ describe("Cipher Service", () => { it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => { const spy = jest .spyOn(apiService, "postCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.createWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.createWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -229,10 +222,8 @@ describe("Cipher Service", () => { it("should call apiService.putCipherAdmin when orgAdmin and isNotClone params are true", async () => { const spy = jest .spyOn(apiService, "putCipherAdmin") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj, true, true); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj, true, true); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -243,10 +234,8 @@ describe("Cipher Service", () => { cipherObj.edit = true; const spy = jest .spyOn(apiService, "putCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherRequest(cipherObj); expect(spy).toHaveBeenCalled(); @@ -257,10 +246,8 @@ describe("Cipher Service", () => { cipherObj.edit = false; const spy = jest .spyOn(apiService, "putPartialCipher") - .mockImplementation(() => Promise.resolve(cipherObj)); - // 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 - cipherService.updateWithServer(cipherObj); + .mockImplementation(() => Promise.resolve(cipherObj.toCipherData())); + await cipherService.updateWithServer(cipherObj); const expectedObj = new CipherPartialRequest(cipherObj); expect(spy).toHaveBeenCalled(); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 7d06b3185f..174da701bd 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1,4 +1,4 @@ -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, firstValueFrom, map, share, skipWhile, switchMap } from "rxjs"; import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; @@ -21,7 +21,13 @@ import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; -import { ActiveUserState, StateProvider } from "../../platform/state"; +import { + ActiveUserState, + CIPHERS_MEMORY, + DeriveDefinition, + DerivedState, + StateProvider, +} from "../../platform/state"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; import { UserKey, OrgKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; @@ -71,10 +77,14 @@ export class CipherService implements CipherServiceAbstraction { private sortedCiphersCache: SortedCiphersCache = new SortedCiphersCache( this.sortCiphersByLastUsed, ); + private ciphersExpectingUpdate: DerivedState; localData$: Observable>; ciphers$: Observable>; cipherViews$: Observable>; + viewFor$(id: CipherId) { + return this.cipherViews$.pipe(map((views) => views[id])); + } addEditCipherInfo$: Observable; private localDataState: ActiveUserState>; @@ -99,10 +109,29 @@ export class CipherService implements CipherServiceAbstraction { this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); + this.ciphersExpectingUpdate = this.stateProvider.getDerived( + this.encryptedCiphersState.state$, + new DeriveDefinition(CIPHERS_MEMORY, "ciphersExpectingUpdate", { + derive: (_: Record) => false, + deserializer: (value) => value, + }), + {}, + ); this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); - this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {})); - this.cipherViews$ = this.decryptedCiphersState.state$.pipe(map((views) => views ?? {})); + // First wait for ciphersExpectingUpdate to be false before emitting ciphers + this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe( + skipWhile((expectingUpdate) => expectingUpdate), + switchMap(() => this.encryptedCiphersState.state$), + map((ciphers) => ciphers ?? {}), + ); + this.cipherViews$ = this.decryptedCiphersState.state$.pipe( + map((views) => views ?? {}), + + share({ + resetOnRefCountZero: true, + }), + ); this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } @@ -573,7 +602,7 @@ export class CipherService implements CipherServiceAbstraction { await this.domainSettingsService.setNeverDomains(domains); } - async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { + async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { let response: CipherResponse; if (orgAdmin && cipher.organizationId != null) { const request = new CipherCreateRequest(cipher); @@ -588,10 +617,16 @@ export class CipherService implements CipherServiceAbstraction { cipher.id = response.id; const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // No local data for new ciphers + return new Cipher(updated[cipher.id as CipherId]); } - async updateWithServer(cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean): Promise { + async updateWithServer( + cipher: Cipher, + orgAdmin?: boolean, + isNotClone?: boolean, + ): Promise { let response: CipherResponse; if (orgAdmin && isNotClone) { const request = new CipherRequest(cipher); @@ -605,7 +640,9 @@ export class CipherService implements CipherServiceAbstraction { } const data = new CipherData(response, cipher.collectionIds); - await this.upsert(data); + const updated = await this.upsert(data); + // updating with server does not change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } async shareWithServer( @@ -732,11 +769,13 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } - async saveCollectionsWithServer(cipher: Cipher): Promise { + async saveCollectionsWithServer(cipher: Cipher): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); const response = await this.apiService.putCipherCollections(cipher.id, request); const data = new CipherData(response); - await this.upsert(data); + const updated = await this.upsert(data); + // Collection updates don't change local data + return new Cipher(updated[cipher.id as CipherId], cipher.localData); } /** @@ -782,9 +821,9 @@ export class CipherService implements CipherServiceAbstraction { await this.encryptedCiphersState.update(() => ciphers); } - async upsert(cipher: CipherData | CipherData[]): Promise { + async upsert(cipher: CipherData | CipherData[]): Promise> { const ciphers = cipher instanceof CipherData ? [cipher] : cipher; - await this.updateEncryptedCipherState((current) => { + return await this.updateEncryptedCipherState((current) => { ciphers.forEach((c) => (current[c.id as CipherId] = c)); return current; }); @@ -796,12 +835,15 @@ export class CipherService implements CipherServiceAbstraction { private async updateEncryptedCipherState( update: (current: Record) => Record, - ) { + ): Promise> { + // Store that we should wait for an update to return any ciphers + await this.ciphersExpectingUpdate.forceValue(true); await this.clearDecryptedCiphersState(); - await this.encryptedCiphersState.update((current) => { + const [, updatedCiphers] = await this.encryptedCiphersState.update((current) => { const result = update(current ?? {}); return result; }); + return updatedCiphers; } async clear(userId?: string): Promise { @@ -1093,7 +1135,9 @@ export class CipherService implements CipherServiceAbstraction { } async setAddEditCipherInfo(value: AddEditCipherInfo) { - await this.addEditCipherInfoState.update(() => value); + await this.addEditCipherInfoState.update(() => value, { + shouldUpdate: (current) => !(current == null && value == null), + }); } // Helpers diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index bfc8cbe915..4e0aab017a 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -48,20 +48,26 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { ) {} async isFido2FeatureEnabled(hostname: string, origin: string): Promise { - const userEnabledPasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$); const isUserLoggedIn = (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; + if (!isUserLoggedIn) { + return false; + } const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const isExcludedDomain = neverDomains != null && hostname in neverDomains; + if (isExcludedDomain) { + return false; + } const serverConfig = await firstValueFrom(this.configService.serverConfig$); const isOriginEqualBitwardenVault = origin === serverConfig.environment?.vault; + if (isOriginEqualBitwardenVault) { + return false; + } - return ( - userEnabledPasskeys && isUserLoggedIn && !isExcludedDomain && !isOriginEqualBitwardenVault - ); + return await firstValueFrom(this.vaultSettingsService.enablePasskeys$); } async createCredential( @@ -70,6 +76,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { abortController = new AbortController(), ): Promise { const parsedOrigin = parse(params.origin, { allowPrivateDomains: true }); + const enableFido2VaultCredentials = await this.isFido2FeatureEnabled( parsedOrigin.hostname, params.origin, @@ -346,7 +353,7 @@ function setAbortTimeout( ); } - return window.setTimeout(() => abortController.abort(), clampedTimeout); + return self.setTimeout(() => abortController.abort(), clampedTimeout); } /** diff --git a/libs/common/src/vault/services/fido2/fido2-utils.spec.ts b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts new file mode 100644 index 0000000000..a05eab5230 --- /dev/null +++ b/libs/common/src/vault/services/fido2/fido2-utils.spec.ts @@ -0,0 +1,40 @@ +import { Fido2Utils } from "./fido2-utils"; + +describe("Fido2 Utils", () => { + const asciiHelloWorldArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]; + const b64HelloWorldString = "aGVsbG8gd29ybGQ="; + + describe("fromBufferToB64(...)", () => { + it("should convert an ArrayBuffer to a b64 string", () => { + const buffer = new Uint8Array(asciiHelloWorldArray).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(b64HelloWorldString); + }); + + it("should return an empty string when given an empty ArrayBuffer", () => { + const buffer = new Uint8Array([]).buffer; + const b64String = Fido2Utils.fromBufferToB64(buffer); + expect(b64String).toBe(""); + }); + + it("should return null when given null input", () => { + const b64String = Fido2Utils.fromBufferToB64(null); + expect(b64String).toBeNull(); + }); + }); + + describe("fromB64ToArray(...)", () => { + it("should convert a b64 string to an Uint8Array", () => { + const expectedArray = new Uint8Array(asciiHelloWorldArray); + + const resultArray = Fido2Utils.fromB64ToArray(b64HelloWorldString); + + expect(resultArray).toEqual(expectedArray); + }); + + it("should return null when given null input", () => { + const expectedArray = Fido2Utils.fromB64ToArray(null); + expect(expectedArray).toBeNull(); + }); + }); +}); diff --git a/libs/common/src/vault/services/fido2/fido2-utils.ts b/libs/common/src/vault/services/fido2/fido2-utils.ts index a2de137550..13c9762135 100644 --- a/libs/common/src/vault/services/fido2/fido2-utils.ts +++ b/libs/common/src/vault/services/fido2/fido2-utils.ts @@ -1,14 +1,20 @@ -import { Utils } from "../../../platform/misc/utils"; - export class Fido2Utils { static bufferToString(bufferSource: BufferSource): string { - const buffer = Fido2Utils.bufferSourceToUint8Array(bufferSource); + let buffer: Uint8Array; + if (bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined) { + buffer = new Uint8Array(bufferSource as ArrayBuffer); + } else { + buffer = new Uint8Array(bufferSource.buffer); + } - return Utils.fromBufferToUrlB64(buffer); + return Fido2Utils.fromBufferToB64(buffer) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); } static stringToBuffer(str: string): Uint8Array { - return Utils.fromUrlB64ToArray(str); + return Fido2Utils.fromB64ToArray(Fido2Utils.fromUrlB64ToB64(str)); } static bufferSourceToUint8Array(bufferSource: BufferSource) { @@ -23,4 +29,52 @@ export class Fido2Utils { private static isArrayBuffer(bufferSource: BufferSource): bufferSource is ArrayBuffer { return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined; } + + static fromB64toUrlB64(b64Str: string) { + return b64Str.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + } + + static fromBufferToB64(buffer: ArrayBuffer): string { + if (buffer == null) { + return null; + } + + let binary = ""; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.byteLength; i++) { + binary += String.fromCharCode(bytes[i]); + } + return globalThis.btoa(binary); + } + + static fromB64ToArray(str: string): Uint8Array { + if (str == null) { + return null; + } + + const binaryString = globalThis.atob(str); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes; + } + + static fromUrlB64ToB64(urlB64Str: string): string { + let output = urlB64Str.replace(/-/g, "+").replace(/_/g, "/"); + switch (output.length % 4) { + case 0: + break; + case 2: + output += "=="; + break; + case 3: + output += "="; + break; + default: + throw new Error("Illegal base64url string!"); + } + + return output; + } } diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index ff8e9f1f4f..793bcf2437 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -15,6 +15,7 @@ import { AccountService } from "../../../auth/abstractions/account.service"; import { AvatarService } from "../../../auth/abstractions/avatar.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction"; +import { TokenService } from "../../../auth/abstractions/token.service"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { DomainSettingsService } from "../../../autofill/services/domain-settings.service"; import { BillingAccountProfileStateService } from "../../../billing/abstractions/account/billing-account-profile-state.service"; @@ -34,7 +35,6 @@ import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "../../../tools/send/services/send.service.abstraction"; -import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; @@ -73,6 +73,7 @@ export class SyncService implements SyncServiceAbstraction { private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, + private tokenService: TokenService, ) {} async getLastSync(): Promise { @@ -309,7 +310,7 @@ export class SyncService implements SyncServiceAbstraction { } private async syncProfile(response: ProfileResponse) { - const stamp = await this.stateService.getSecurityStamp(); + const stamp = await this.tokenService.getSecurityStamp(response.id); if (stamp != null && stamp !== response.securityStamp) { if (this.logoutCallback != null) { await this.logoutCallback(true); @@ -319,12 +320,16 @@ export class SyncService implements SyncServiceAbstraction { } await this.cryptoService.setMasterKeyEncryptedUserKey(response.key); - await this.cryptoService.setPrivateKey(response.privateKey); - await this.cryptoService.setProviderKeys(response.providers); - await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); - await this.stateService.setSecurityStamp(response.securityStamp); - await this.stateService.setEmailVerified(response.emailVerified); + await this.cryptoService.setPrivateKey(response.privateKey, response.id); + await this.cryptoService.setProviderKeys(response.providers, response.id); + await this.cryptoService.setOrgKeys( + response.organizations, + response.providerOrganizations, + response.id, + ); + await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor); + await this.tokenService.setSecurityStamp(response.securityStamp, response.id); + await this.accountService.setAccountEmailVerified(response.id, response.emailVerified); await this.billingAccountProfileStateService.setHasPremium( response.premiumPersonally, diff --git a/libs/components/src/a11y/a11y-cell.directive.ts b/libs/components/src/a11y/a11y-cell.directive.ts new file mode 100644 index 0000000000..fdd75c076f --- /dev/null +++ b/libs/components/src/a11y/a11y-cell.directive.ts @@ -0,0 +1,33 @@ +import { ContentChild, Directive, ElementRef, HostBinding } from "@angular/core"; + +import { FocusableElement } from "../shared/focusable-element"; + +@Directive({ + selector: "bitA11yCell", + standalone: true, + providers: [{ provide: FocusableElement, useExisting: A11yCellDirective }], +}) +export class A11yCellDirective implements FocusableElement { + @HostBinding("attr.role") + role: "gridcell" | null; + + @ContentChild(FocusableElement) + private focusableChild: FocusableElement; + + getFocusTarget() { + let focusTarget: HTMLElement; + if (this.focusableChild) { + focusTarget = this.focusableChild.getFocusTarget(); + } else { + focusTarget = this.elementRef.nativeElement.querySelector("button, a"); + } + + if (!focusTarget) { + return this.elementRef.nativeElement; + } + + return focusTarget; + } + + constructor(private elementRef: ElementRef) {} +} diff --git a/libs/components/src/a11y/a11y-grid.directive.ts b/libs/components/src/a11y/a11y-grid.directive.ts new file mode 100644 index 0000000000..c632376f4f --- /dev/null +++ b/libs/components/src/a11y/a11y-grid.directive.ts @@ -0,0 +1,145 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + HostListener, + Input, + QueryList, +} from "@angular/core"; + +import type { A11yCellDirective } from "./a11y-cell.directive"; +import { A11yRowDirective } from "./a11y-row.directive"; + +@Directive({ + selector: "bitA11yGrid", + standalone: true, +}) +export class A11yGridDirective implements AfterViewInit { + @HostBinding("attr.role") + role = "grid"; + + @ContentChildren(A11yRowDirective) + rows: QueryList; + + /** The number of pages to navigate on `PageUp` and `PageDown` */ + @Input() pageSize = 5; + + private grid: A11yCellDirective[][]; + + /** The row that currently has focus */ + private activeRow = 0; + + /** The cell that currently has focus */ + private activeCol = 0; + + @HostListener("keydown", ["$event"]) + onKeyDown(event: KeyboardEvent) { + switch (event.code) { + case "ArrowUp": + this.updateCellFocusByDelta(-1, 0); + break; + case "ArrowRight": + this.updateCellFocusByDelta(0, 1); + break; + case "ArrowDown": + this.updateCellFocusByDelta(1, 0); + break; + case "ArrowLeft": + this.updateCellFocusByDelta(0, -1); + break; + case "Home": + this.updateCellFocusByDelta(-this.activeRow, -this.activeCol); + break; + case "End": + this.updateCellFocusByDelta(this.grid.length, this.grid[this.grid.length - 1].length); + break; + case "PageUp": + this.updateCellFocusByDelta(-this.pageSize, 0); + break; + case "PageDown": + this.updateCellFocusByDelta(this.pageSize, 0); + break; + default: + return; + } + + /** Prevent default scrolling behavior */ + event.preventDefault(); + } + + ngAfterViewInit(): void { + this.initializeGrid(); + } + + private initializeGrid(): void { + try { + this.grid = this.rows.map((listItem) => { + listItem.role = "row"; + return [...listItem.cells]; + }); + this.grid.flat().forEach((cell) => { + cell.role = "gridcell"; + cell.getFocusTarget().tabIndex = -1; + }); + + this.getActiveCellContent().tabIndex = 0; + } catch (error) { + // eslint-disable-next-line no-console + console.error("Unable to initialize grid"); + } + } + + /** Get the focusable content of the active cell */ + private getActiveCellContent(): HTMLElement { + return this.grid[this.activeRow][this.activeCol].getFocusTarget(); + } + + /** Move focus via a delta against the currently active gridcell */ + private updateCellFocusByDelta(rowDelta: number, colDelta: number) { + const prevActive = this.getActiveCellContent(); + + this.activeCol += colDelta; + this.activeRow += rowDelta; + + // Row upper bound + if (this.activeRow >= this.grid.length) { + this.activeRow = this.grid.length - 1; + } + + // Row lower bound + if (this.activeRow < 0) { + this.activeRow = 0; + } + + // Column upper bound + if (this.activeCol >= this.grid[this.activeRow].length) { + if (this.activeRow < this.grid.length - 1) { + // Wrap to next row on right arrow + this.activeCol = 0; + this.activeRow += 1; + } else { + this.activeCol = this.grid[this.activeRow].length - 1; + } + } + + // Column lower bound + if (this.activeCol < 0) { + if (this.activeRow > 0) { + // Wrap to prev row on left arrow + this.activeRow -= 1; + this.activeCol = this.grid[this.activeRow].length - 1; + } else { + this.activeCol = 0; + } + } + + const nextActive = this.getActiveCellContent(); + nextActive.tabIndex = 0; + nextActive.focus(); + + if (nextActive !== prevActive) { + prevActive.tabIndex = -1; + } + } +} diff --git a/libs/components/src/a11y/a11y-row.directive.ts b/libs/components/src/a11y/a11y-row.directive.ts new file mode 100644 index 0000000000..e062eb2b5a --- /dev/null +++ b/libs/components/src/a11y/a11y-row.directive.ts @@ -0,0 +1,31 @@ +import { + AfterViewInit, + ContentChildren, + Directive, + HostBinding, + QueryList, + ViewChildren, +} from "@angular/core"; + +import { A11yCellDirective } from "./a11y-cell.directive"; + +@Directive({ + selector: "bitA11yRow", + standalone: true, +}) +export class A11yRowDirective implements AfterViewInit { + @HostBinding("attr.role") + role: "row" | null; + + cells: A11yCellDirective[]; + + @ViewChildren(A11yCellDirective) + private viewCells: QueryList; + + @ContentChildren(A11yCellDirective) + private contentCells: QueryList; + + ngAfterViewInit(): void { + this.cells = [...this.viewCells, ...this.contentCells]; + } +} diff --git a/libs/components/src/badge/badge.directive.ts b/libs/components/src/badge/badge.directive.ts index b81b9f80e2..acce4a18aa 100644 --- a/libs/components/src/badge/badge.directive.ts +++ b/libs/components/src/badge/badge.directive.ts @@ -1,5 +1,7 @@ import { Directive, ElementRef, HostBinding, Input } from "@angular/core"; +import { FocusableElement } from "../shared/focusable-element"; + export type BadgeVariant = "primary" | "secondary" | "success" | "danger" | "warning" | "info"; const styles: Record = { @@ -22,8 +24,9 @@ const hoverStyles: Record = { @Directive({ selector: "span[bitBadge], a[bitBadge], button[bitBadge]", + providers: [{ provide: FocusableElement, useExisting: BadgeDirective }], }) -export class BadgeDirective { +export class BadgeDirective implements FocusableElement { @HostBinding("class") get classList() { return [ "tw-inline-block", @@ -62,6 +65,10 @@ export class BadgeDirective { */ @Input() truncate = true; + getFocusTarget() { + return this.el.nativeElement; + } + private hasHoverEffects = false; constructor(private el: ElementRef) { diff --git a/libs/components/src/card/card.component.ts b/libs/components/src/card/card.component.ts new file mode 100644 index 0000000000..da61d53664 --- /dev/null +++ b/libs/components/src/card/card.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-card", + standalone: true, + imports: [CommonModule], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: + "tw-box-border tw-block tw-bg-background tw-text-main tw-border-solid tw-border-b tw-border-0 tw-border-b-secondary-300 tw-rounded-lg tw-py-4 tw-px-3", + }, +}) +export class CardComponent {} diff --git a/libs/components/src/card/card.stories.ts b/libs/components/src/card/card.stories.ts new file mode 100644 index 0000000000..702a8aeb63 --- /dev/null +++ b/libs/components/src/card/card.stories.ts @@ -0,0 +1,62 @@ +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { SectionComponent } from "../section"; +import { TypographyModule } from "../typography"; + +import { CardComponent } from "./card.component"; + +export default { + title: "Component Library/Card", + component: CardComponent, + decorators: [ + moduleMetadata({ + imports: [TypographyModule, SectionComponent], + }), + componentWrapperDecorator( + (story) => `
${story}
`, + ), + ], +} as Meta; + +type Story = StoryObj; + +/** Cards are presentational containers. */ +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+ `, + }), +}; + +/** Cards are often paired with [Sections](/docs/component-library-section--docs). */ +export const WithinSections: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ + +

Bar

+ +

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras vitae congue risus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Nunc elementum odio nibh, eget pellentesque sem ornare vitae. Etiam vel ante et velit fringilla egestas a sed sem. Fusce molestie nisl et nisi accumsan dapibus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed eu risus ex.

+
+
+ `, + }), +}; diff --git a/libs/components/src/card/index.ts b/libs/components/src/card/index.ts new file mode 100644 index 0000000000..8151bac4c8 --- /dev/null +++ b/libs/components/src/card/index.ts @@ -0,0 +1 @@ +export * from "./card.component"; diff --git a/libs/components/src/dialog/dialogs.mdx b/libs/components/src/dialog/dialogs.mdx index 8ff46ad381..bd6a30d7f2 100644 --- a/libs/components/src/dialog/dialogs.mdx +++ b/libs/components/src/dialog/dialogs.mdx @@ -50,7 +50,7 @@ element after close since a user may not want to close the dialog immediately if additional interactive elements. See [WCAG Focus Order success criteria](https://www.w3.org/WAI/WCAG21/Understanding/focus-order.html) -Once closed, focus should remain on the the element which triggered the Dialog. +Once closed, focus should remain on the element which triggered the Dialog. **Note:** If a Simple Dialog is triggered from a main Dialog, be sure to make sure focus is moved to the Simple Dialog. diff --git a/libs/components/src/form/forms.mdx b/libs/components/src/form/forms.mdx index a42ddccbe6..6c6fa33f68 100644 --- a/libs/components/src/form/forms.mdx +++ b/libs/components/src/form/forms.mdx @@ -16,8 +16,8 @@ always use the native `form` element and bind a `formGroup`. Forms consists of 1 or more inputs, and ends with 1 or 2 buttons. -If there are many inputs in a form, they should should be organized into sections as content -relates. **Example:** Item type form +If there are many inputs in a form, they should be organized into sections as content relates. +**Example:** Item type form Each input within a section should follow the following spacing guidelines (see [Tailwind CSS spacing documentation](https://tailwindcss.com/docs/customizing-spacing)): diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index 53e8032795..54f6dfda96 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -1,6 +1,7 @@ -import { Component, HostBinding, Input } from "@angular/core"; +import { Component, ElementRef, HostBinding, Input } from "@angular/core"; import { ButtonLikeAbstraction, ButtonType } from "../shared/button-like.abstraction"; +import { FocusableElement } from "../shared/focusable-element"; export type IconButtonType = ButtonType | "contrast" | "main" | "muted" | "light"; @@ -123,9 +124,12 @@ const sizes: Record = { @Component({ selector: "button[bitIconButton]:not(button[bitButton])", templateUrl: "icon-button.component.html", - providers: [{ provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }], + providers: [ + { provide: ButtonLikeAbstraction, useExisting: BitIconButtonComponent }, + { provide: FocusableElement, useExisting: BitIconButtonComponent }, + ], }) -export class BitIconButtonComponent implements ButtonLikeAbstraction { +export class BitIconButtonComponent implements ButtonLikeAbstraction, FocusableElement { @Input("bitIconButton") icon: string; @Input() buttonType: IconButtonType; @@ -162,4 +166,10 @@ export class BitIconButtonComponent implements ButtonLikeAbstraction { setButtonType(value: "primary" | "secondary" | "danger" | "unstyled") { this.buttonType = value; } + + getFocusTarget() { + return this.elementRef.nativeElement; + } + + constructor(private elementRef: ElementRef) {} } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 139e69ebb6..1e4a3a86ff 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,6 +7,7 @@ export * from "./breadcrumbs"; export * from "./button"; export { ButtonType } from "./shared/button-like.abstraction"; export * from "./callout"; +export * from "./card"; export * from "./checkbox"; export * from "./color-password"; export * from "./container"; @@ -15,6 +16,7 @@ export * from "./form-field"; export * from "./icon-button"; export * from "./icon"; export * from "./input"; +export * from "./item"; export * from "./layout"; export * from "./link"; export * from "./menu"; @@ -29,6 +31,7 @@ export * from "./section"; export * from "./select"; export * from "./table"; export * from "./tabs"; +export * from "./toast"; export * from "./toggle-group"; export * from "./typography"; export * from "./utils/i18n-mock.service"; diff --git a/libs/components/src/input/autofocus.directive.ts b/libs/components/src/input/autofocus.directive.ts index f8161ee6e0..625e7fbc92 100644 --- a/libs/components/src/input/autofocus.directive.ts +++ b/libs/components/src/input/autofocus.directive.ts @@ -3,12 +3,7 @@ import { take } from "rxjs/operators"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -/** - * Interface for implementing focusable components. Used by the AutofocusDirective. - */ -export abstract class FocusableElement { - focus: () => void; -} +import { FocusableElement } from "../shared/focusable-element"; /** * Directive to focus an element. @@ -46,7 +41,7 @@ export class AutofocusDirective { private focus() { if (this.focusableElement) { - this.focusableElement.focus(); + this.focusableElement.getFocusTarget().focus(); } else { this.el.nativeElement.focus(); } diff --git a/libs/components/src/item/index.ts b/libs/components/src/item/index.ts new file mode 100644 index 0000000000..56896cdc3c --- /dev/null +++ b/libs/components/src/item/index.ts @@ -0,0 +1 @@ +export * from "./item.module"; diff --git a/libs/components/src/item/item-action.component.ts b/libs/components/src/item/item-action.component.ts new file mode 100644 index 0000000000..8cabf5c5c2 --- /dev/null +++ b/libs/components/src/item/item-action.component.ts @@ -0,0 +1,12 @@ +import { Component } from "@angular/core"; + +import { A11yCellDirective } from "../a11y/a11y-cell.directive"; + +@Component({ + selector: "bit-item-action", + standalone: true, + imports: [], + template: ``, + providers: [{ provide: A11yCellDirective, useExisting: ItemActionComponent }], +}) +export class ItemActionComponent extends A11yCellDirective {} diff --git a/libs/components/src/item/item-content.component.html b/libs/components/src/item/item-content.component.html new file mode 100644 index 0000000000..da69c79c1e --- /dev/null +++ b/libs/components/src/item/item-content.component.html @@ -0,0 +1,16 @@ +
+ + +
+
+ +
+
+ +
+
+
+ +
+ +
diff --git a/libs/components/src/item/item-content.component.ts b/libs/components/src/item/item-content.component.ts new file mode 100644 index 0000000000..58a1198512 --- /dev/null +++ b/libs/components/src/item/item-content.component.ts @@ -0,0 +1,15 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-content, [bit-item-content]", + standalone: true, + imports: [CommonModule], + templateUrl: `item-content.component.html`, + host: { + class: + "fvw-target tw-outline-none tw-text-main hover:tw-text-main hover:tw-no-underline tw-text-base tw-py-2 tw-px-4 tw-bg-transparent tw-w-full tw-border-none tw-flex tw-gap-4 tw-items-center tw-justify-between", + }, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemContentComponent {} diff --git a/libs/components/src/item/item-group.component.ts b/libs/components/src/item/item-group.component.ts new file mode 100644 index 0000000000..2a9a8275cc --- /dev/null +++ b/libs/components/src/item/item-group.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from "@angular/core"; + +@Component({ + selector: "bit-item-group", + standalone: true, + imports: [], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: "tw-block", + }, +}) +export class ItemGroupComponent {} diff --git a/libs/components/src/item/item.component.html b/libs/components/src/item/item.component.html new file mode 100644 index 0000000000..c02117058f --- /dev/null +++ b/libs/components/src/item/item.component.html @@ -0,0 +1,21 @@ + +
+ + + + +
+ +
+
diff --git a/libs/components/src/item/item.component.ts b/libs/components/src/item/item.component.ts new file mode 100644 index 0000000000..4b7b57fa9f --- /dev/null +++ b/libs/components/src/item/item.component.ts @@ -0,0 +1,29 @@ +import { CommonModule } from "@angular/common"; +import { ChangeDetectionStrategy, Component, HostListener, signal } from "@angular/core"; + +import { A11yRowDirective } from "../a11y/a11y-row.directive"; + +import { ItemActionComponent } from "./item-action.component"; + +@Component({ + selector: "bit-item", + standalone: true, + imports: [CommonModule, ItemActionComponent], + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: "item.component.html", + providers: [{ provide: A11yRowDirective, useExisting: ItemComponent }], +}) +export class ItemComponent extends A11yRowDirective { + /** + * We have `:focus-within` and `:focus-visible` but no `:focus-visible-within` + */ + protected focusVisibleWithin = signal(false); + @HostListener("focusin", ["$event.target"]) + onFocusIn(target: HTMLElement) { + this.focusVisibleWithin.set(target.matches(".fvw-target:focus-visible")); + } + @HostListener("focusout") + onFocusOut() { + this.focusVisibleWithin.set(false); + } +} diff --git a/libs/components/src/item/item.mdx b/libs/components/src/item/item.mdx new file mode 100644 index 0000000000..8506de72bb --- /dev/null +++ b/libs/components/src/item/item.mdx @@ -0,0 +1,141 @@ +import { Meta, Story, Primary, Controls, Canvas } from "@storybook/addon-docs"; + +import * as stories from "./item.stories"; + + + +```ts +import { ItemModule } from "@bitwarden/components"; +``` + +# Item + +`` is a horizontal card that contains one or more interactive actions. + +It is a generic container that can be used for either standalone content, an alternative to tables, +or to list nav links. + + + + + +## Primary Content + +The primary content of an item is supplied by `bit-item-content`. + +### Content Types + +The content can be a button, anchor, or static container. + +```html + + Hi, I am a link. + + + + + + + + I'm just static :( + +``` + + + + + +### Content Slots + +`bit-item-content` contains the following slots to help position the content: + +| Slot | Description | +| ------------------ | --------------------------------------------------- | +| default | primary text or arbitrary content; fan favorite | +| `slot="secondary"` | supporting text; under the default slot | +| `slot="start"` | commonly an icon or avatar; before the default slot | +| `slot="end"` | commonly an icon; after the default slot | + +- Note: There is also an `end` slot within `bit-item` itself. Place + [interactive secondary actions](#secondary-actions) there, and place non-interactive content (such + as icons) in `bit-item-content` + +```html + + + +``` + + + + + +## Secondary Actions + +Secondary interactive actions can be placed in the item through the `"end"` slot, outside of +`bit-item-content`. + +Each action must be wrapped by ``. + +Actions are commonly icon buttons or badge buttons. + +```html + + + + + + + + + + + + + + + +``` + +## Item Groups + +Groups of items can be associated by wrapping them in the ``. + + + + + + + + + +### A11y + +Keyboard nav is currently disabled due to a bug when used within a virtual scroll viewport. + +Item groups utilize arrow-based keyboard navigation +([further reading here](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#kbd_label)). + +Use `aria-label` or `aria-labelledby` to give groups an accessible name. + +```html + + ... + ... + ... + +``` + +### Virtual Scrolling + + + + diff --git a/libs/components/src/item/item.module.ts b/libs/components/src/item/item.module.ts new file mode 100644 index 0000000000..226fed11d8 --- /dev/null +++ b/libs/components/src/item/item.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from "@angular/core"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +@NgModule({ + imports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], + exports: [ItemComponent, ItemContentComponent, ItemActionComponent, ItemGroupComponent], +}) +export class ItemModule {} diff --git a/libs/components/src/item/item.stories.ts b/libs/components/src/item/item.stories.ts new file mode 100644 index 0000000000..5198d7efa6 --- /dev/null +++ b/libs/components/src/item/item.stories.ts @@ -0,0 +1,334 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; +import { CommonModule } from "@angular/common"; +import { Meta, StoryObj, componentWrapperDecorator, moduleMetadata } from "@storybook/angular"; + +import { A11yGridDirective } from "../a11y/a11y-grid.directive"; +import { AvatarModule } from "../avatar"; +import { BadgeModule } from "../badge"; +import { IconButtonModule } from "../icon-button"; +import { TypographyModule } from "../typography"; + +import { ItemActionComponent } from "./item-action.component"; +import { ItemContentComponent } from "./item-content.component"; +import { ItemGroupComponent } from "./item-group.component"; +import { ItemComponent } from "./item.component"; + +export default { + title: "Component Library/Item", + component: ItemComponent, + decorators: [ + moduleMetadata({ + imports: [ + CommonModule, + ItemGroupComponent, + AvatarModule, + IconButtonModule, + BadgeModule, + TypographyModule, + ItemActionComponent, + ItemContentComponent, + A11yGridDirective, + ScrollingModule, + ], + }), + componentWrapperDecorator((story) => `
${story}
`), + ], +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + `, + }), +}; + +export const ContentSlots: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + `, + }), +}; + +export const ContentTypes: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + Hi, I am a link. + + + + + + + + I'm just static :( + + + `, + }), +}; + +export const TextOverflow: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + Helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo! + Worlddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd! + + + + + + + + + + + `, + }), +}; + +export const MultipleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + }), +}; + +export const SingleActionList: Story = { + render: (args) => ({ + props: args, + template: /*html*/ ` + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + + Foobar + + + + + `, + }), +}; + +export const VirtualScrolling: Story = { + render: (_args) => ({ + props: { + data: Array.from(Array(100000).keys()), + }, + template: /*html*/ ` + + + + + + + + + + + + + + + + + + + + `, + }), +}; diff --git a/libs/components/src/search/search.component.ts b/libs/components/src/search/search.component.ts index a0f3eb363f..27170d5d7b 100644 --- a/libs/components/src/search/search.component.ts +++ b/libs/components/src/search/search.component.ts @@ -1,7 +1,7 @@ import { Component, ElementRef, Input, ViewChild } from "@angular/core"; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { FocusableElement } from "../input/autofocus.directive"; +import { FocusableElement } from "../shared/focusable-element"; let nextId = 0; @@ -32,8 +32,8 @@ export class SearchComponent implements ControlValueAccessor, FocusableElement { @Input() disabled: boolean; @Input() placeholder: string; - focus() { - this.input.nativeElement.focus(); + getFocusTarget() { + return this.input.nativeElement; } onChange(searchText: string) { diff --git a/libs/components/src/section/section.component.ts b/libs/components/src/section/section.component.ts index a681dcf7d9..a60e232eec 100644 --- a/libs/components/src/section/section.component.ts +++ b/libs/components/src/section/section.component.ts @@ -6,7 +6,7 @@ import { Component } from "@angular/core"; standalone: true, imports: [CommonModule], template: ` -
+
`, diff --git a/libs/components/src/section/section.stories.ts b/libs/components/src/section/section.stories.ts index fb9948e9be..65b6a67d47 100644 --- a/libs/components/src/section/section.stories.ts +++ b/libs/components/src/section/section.stories.ts @@ -17,7 +17,7 @@ export default { type Story = StoryObj; -/** Sections are simple containers that apply a bottom margin. They often contain a heading. */ +/** Sections are simple containers that apply a responsive bottom margin. They often contain a heading. */ export const Default: Story = { render: (args) => ({ props: args, diff --git a/libs/components/src/shared/focusable-element.ts b/libs/components/src/shared/focusable-element.ts new file mode 100644 index 0000000000..1ea422aa6f --- /dev/null +++ b/libs/components/src/shared/focusable-element.ts @@ -0,0 +1,8 @@ +/** + * Interface for implementing focusable components. + * + * Used by the `AutofocusDirective` and `A11yGridDirective`. + */ +export abstract class FocusableElement { + getFocusTarget: () => HTMLElement; +} diff --git a/libs/components/src/stories/icons.mdx b/libs/components/src/stories/icons.mdx index 44bae3a54f..70fe18b34a 100644 --- a/libs/components/src/stories/icons.mdx +++ b/libs/components/src/stories/icons.mdx @@ -41,6 +41,7 @@ or an options menu icon. | | bwi-sticky-note | secure note item type | | | bwi-users | user group | | | bwi-vault | general vault | +| | bwi-vault-f | general vault | ## Actions @@ -68,8 +69,10 @@ or an options menu icon. | | bwi-minus-square | unselect all action | | | bwi-paste | paste from clipboard action | | | bwi-pencil-square | edit action | +| | bwi-popout | popout action | | | bwi-play | start or play action | | | bwi-plus | new or add option in contained buttons/links | +| | bwi-plus-f | new or add option in contained buttons/links | | | bwi-plus-circle | new or add option in text buttons/links | | | bwi-plus-square | - | | | bwi-refresh | "re"-action; such as refresh or regenerate | @@ -101,6 +104,7 @@ or an options menu icon. | | bwi-arrow-circle-left | - | | | bwi-arrow-circle-right | - | | | bwi-arrow-circle-up | - | +| | bwi-back | back arrow | | | bwi-caret-down | table sort order | | | bwi-caret-right | - | | | bwi-caret-up | table sort order | @@ -128,6 +132,7 @@ or an options menu icon. | | bwi-bolt | deprecated "danger" icon | | | bwi-bookmark | bookmark or save related actions | | | bwi-browser | web browser | +| | bwi-browser-alt | web browser | | | bwi-bug | test or debug action | | | bwi-camera | actions related to camera use | | | bwi-chain-broken | unlink action | @@ -138,6 +143,7 @@ or an options menu icon. | | bwi-cut | cut or omit actions | | | bwi-dashboard | statuses or dashboard views | | | bwi-desktop | desktop client | +| | bwi-desktop-alt | desktop client | | | bwi-dollar | account credit | | | bwi-file | file related objects or actions | | | bwi-file-pdf | PDF related object or actions | @@ -153,7 +159,9 @@ or an options menu icon. | | bwi-lightbulb | - | | | bwi-link | link action | | | bwi-mobile | mobile client | +| | bwi-mobile-alt | mobile client | | | bwi-money | - | +| | bwi-msp | - | | | bwi-paperclip | attachments | | | bwi-passkey | passkey | | | bwi-pencil | editing | @@ -176,6 +184,8 @@ or an options menu icon. | | bwi-user | relates to current user or organization member | | | bwi-user-circle | - | | | bwi-user-f | - | +| | bwi-user-monitor | - | +| | bwi-wand | - | | | bwi-wireless | - | | | bwi-wrench | tools or additional configuration options | @@ -203,4 +213,5 @@ or an options menu icon. | | bwi-twitch | link to our Twitch page | | | bwi-twitter | link to our twitter page | | | bwi-windows | support for windows | +| | bwi-x-twitter | x version of twitter | | | bwi-youtube | link to our youtube page | diff --git a/libs/components/src/styles.scss b/libs/components/src/styles.scss index f03d2dd340..7ddcb1b64b 100644 --- a/libs/components/src/styles.scss +++ b/libs/components/src/styles.scss @@ -47,3 +47,8 @@ $card-icons-base: "../../src/billing/images/cards/"; @import "bootstrap/scss/_print"; @import "multi-select/scss/bw.theme.scss"; + +// Workaround for https://bitwarden.atlassian.net/browse/CL-110 +.sbdocs-preview pre.prismjs { + color: white; +} diff --git a/libs/components/src/toast/index.ts b/libs/components/src/toast/index.ts new file mode 100644 index 0000000000..f0b5540219 --- /dev/null +++ b/libs/components/src/toast/index.ts @@ -0,0 +1,2 @@ +export * from "./toast.module"; +export * from "./toast.service"; diff --git a/libs/components/src/toast/toast.component.html b/libs/components/src/toast/toast.component.html new file mode 100644 index 0000000000..f301995d0a --- /dev/null +++ b/libs/components/src/toast/toast.component.html @@ -0,0 +1,24 @@ +
+
+ +
+ {{ variant | i18n }} +

{{ title }}

+

+ {{ m }} +

+
+ +
+
+
diff --git a/libs/components/src/toast/toast.component.ts b/libs/components/src/toast/toast.component.ts new file mode 100644 index 0000000000..4a31e00586 --- /dev/null +++ b/libs/components/src/toast/toast.component.ts @@ -0,0 +1,66 @@ +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { IconButtonModule } from "../icon-button"; +import { SharedModule } from "../shared"; + +export type ToastVariant = "success" | "error" | "info" | "warning"; + +const variants: Record = { + success: { + icon: "bwi-check", + bgColor: "tw-bg-success-600", + }, + error: { + icon: "bwi-error", + bgColor: "tw-bg-danger-600", + }, + info: { + icon: "bwi-info-circle", + bgColor: "tw-bg-info-600", + }, + warning: { + icon: "bwi-exclamation-triangle", + bgColor: "tw-bg-warning-600", + }, +}; + +@Component({ + selector: "bit-toast", + templateUrl: "toast.component.html", + standalone: true, + imports: [SharedModule, IconButtonModule], +}) +export class ToastComponent { + @Input() variant: ToastVariant = "info"; + + /** + * The message to display + * + * Pass an array to render multiple paragraphs. + **/ + @Input({ required: true }) + message: string | string[]; + + /** An optional title to display over the message. */ + @Input() title: string; + + /** + * The percent width of the progress bar, from 0-100 + **/ + @Input() progressWidth = 0; + + /** Emits when the user presses the close button */ + @Output() onClose = new EventEmitter(); + + protected get iconClass(): string { + return variants[this.variant].icon; + } + + protected get bgColor(): string { + return variants[this.variant].bgColor; + } + + protected get messageArray(): string[] { + return Array.isArray(this.message) ? this.message : [this.message]; + } +} diff --git a/libs/components/src/toast/toast.module.ts b/libs/components/src/toast/toast.module.ts new file mode 100644 index 0000000000..bf39a0be9a --- /dev/null +++ b/libs/components/src/toast/toast.module.ts @@ -0,0 +1,39 @@ +import { CommonModule } from "@angular/common"; +import { ModuleWithProviders, NgModule } from "@angular/core"; +import { DefaultNoComponentGlobalConfig, GlobalConfig, TOAST_CONFIG } from "ngx-toastr"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrComponent } from "./toastr.component"; + +@NgModule({ + imports: [CommonModule, ToastComponent], + declarations: [BitwardenToastrComponent], + exports: [BitwardenToastrComponent], +}) +export class ToastModule { + static forRoot(config: Partial = {}): ModuleWithProviders { + return { + ngModule: ToastModule, + providers: [ + { + provide: TOAST_CONFIG, + useValue: { + default: BitwardenToastrGlobalConfig, + config: config, + }, + }, + ], + }; + } +} + +export const BitwardenToastrGlobalConfig: GlobalConfig = { + ...DefaultNoComponentGlobalConfig, + toastComponent: BitwardenToastrComponent, + tapToDismiss: false, + timeOut: 5000, + extendedTimeOut: 2000, + maxOpened: 5, + autoDismiss: true, + progressBar: true, +}; diff --git a/libs/components/src/toast/toast.service.ts b/libs/components/src/toast/toast.service.ts new file mode 100644 index 0000000000..8bbff02c41 --- /dev/null +++ b/libs/components/src/toast/toast.service.ts @@ -0,0 +1,57 @@ +import { Injectable } from "@angular/core"; +import { IndividualConfig, ToastrService } from "ngx-toastr"; + +import type { ToastComponent } from "./toast.component"; +import { calculateToastTimeout } from "./utils"; + +export type ToastOptions = { + /** + * The duration the toast will persist in milliseconds + **/ + timeout?: number; +} & Pick; + +/** + * Presents toast notifications + **/ +@Injectable({ providedIn: "root" }) +export class ToastService { + constructor(private toastrService: ToastrService) {} + + showToast(options: ToastOptions) { + const toastrConfig: Partial = { + payload: { + message: options.message, + variant: options.variant, + title: options.title, + }, + timeOut: + options.timeout != null && options.timeout > 0 + ? options.timeout + : calculateToastTimeout(options.message), + }; + + this.toastrService.show(null, options.title, toastrConfig); + } + + /** + * @deprecated use `showToast` instead + * + * Converts options object from PlatformUtilsService + **/ + _showToast(options: { + type: "error" | "success" | "warning" | "info"; + title: string; + text: string | string[]; + options?: { + timeout?: number; + }; + }) { + this.showToast({ + message: options.text, + variant: options.type, + title: options.title, + timeout: options.options?.timeout, + }); + } +} diff --git a/libs/components/src/toast/toast.spec.ts b/libs/components/src/toast/toast.spec.ts new file mode 100644 index 0000000000..92d8071dc5 --- /dev/null +++ b/libs/components/src/toast/toast.spec.ts @@ -0,0 +1,16 @@ +import { calculateToastTimeout } from "./utils"; + +describe("Toast default timer", () => { + it("should have a minimum of 5000ms", () => { + expect(calculateToastTimeout("")).toBe(5000); + expect(calculateToastTimeout([""])).toBe(5000); + expect(calculateToastTimeout(" ")).toBe(5000); + }); + + it("should return an extra second for each 120 words", () => { + expect(calculateToastTimeout("foo ".repeat(119))).toBe(5000); + expect(calculateToastTimeout("foo ".repeat(120))).toBe(6000); + expect(calculateToastTimeout("foo ".repeat(240))).toBe(7000); + expect(calculateToastTimeout(["foo ".repeat(120), " \n foo ".repeat(120)])).toBe(7000); + }); +}); diff --git a/libs/components/src/toast/toast.stories.ts b/libs/components/src/toast/toast.stories.ts new file mode 100644 index 0000000000..d209453d85 --- /dev/null +++ b/libs/components/src/toast/toast.stories.ts @@ -0,0 +1,124 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; +import { action } from "@storybook/addon-actions"; +import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular"; + +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; + +import { ButtonModule } from "../button"; +import { I18nMockService } from "../utils/i18n-mock.service"; + +import { ToastComponent } from "./toast.component"; +import { BitwardenToastrGlobalConfig, ToastModule } from "./toast.module"; +import { ToastOptions, ToastService } from "./toast.service"; + +const toastServiceExampleTemplate = ` + +`; +@Component({ + selector: "toast-service-example", + template: toastServiceExampleTemplate, +}) +export class ToastServiceExampleComponent { + @Input() + toastOptions: ToastOptions; + + constructor(protected toastService: ToastService) {} +} + +export default { + title: "Component Library/Toast", + component: ToastComponent, + + decorators: [ + moduleMetadata({ + imports: [CommonModule, BrowserAnimationsModule, ButtonModule], + declarations: [ToastServiceExampleComponent], + }), + applicationConfig({ + providers: [ + ToastModule.forRoot().providers, + { + provide: I18nService, + useFactory: () => { + return new I18nMockService({ + close: "Close", + success: "Success", + error: "Error", + warning: "Warning", + }); + }, + }, + ], + }), + ], + args: { + onClose: action("emit onClose"), + variant: "info", + progressWidth: 50, + title: "", + message: "Hello Bitwarden!", + }, + parameters: { + design: { + type: "figma", + url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library", + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: (args) => ({ + props: args, + template: ` +
+ + + + +
+ `, + }), +}; + +/** + * Avoid using long messages in toasts. + */ +export const LongContent: Story = { + ...Default, + args: { + title: "Foo", + message: [ + "Lorem ipsum dolor sit amet, consectetur adipisci", + "Lorem ipsum dolor sit amet, consectetur adipisci", + ], + }, +}; + +export const Service: Story = { + render: (args) => ({ + props: { + toastOptions: args, + }, + template: ` + + `, + }), + args: { + title: "", + message: "Hello Bitwarden!", + variant: "error", + timeout: BitwardenToastrGlobalConfig.timeOut, + } as ToastOptions, + parameters: { + chromatic: { disableSnapshot: true }, + docs: { + source: { + code: toastServiceExampleTemplate, + }, + }, + }, +}; diff --git a/libs/components/src/toast/toast.tokens.css b/libs/components/src/toast/toast.tokens.css new file mode 100644 index 0000000000..2ff9e99ae5 --- /dev/null +++ b/libs/components/src/toast/toast.tokens.css @@ -0,0 +1,4 @@ +:root { + --bit-toast-width: 19rem; + --bit-toast-width-full: 96%; +} diff --git a/libs/components/src/toast/toastr.component.ts b/libs/components/src/toast/toastr.component.ts new file mode 100644 index 0000000000..70085dfc47 --- /dev/null +++ b/libs/components/src/toast/toastr.component.ts @@ -0,0 +1,26 @@ +import { animate, state, style, transition, trigger } from "@angular/animations"; +import { Component } from "@angular/core"; +import { Toast as BaseToastrComponent } from "ngx-toastr"; + +@Component({ + template: ` + + `, + animations: [ + trigger("flyInOut", [ + state("inactive", style({ opacity: 0 })), + state("active", style({ opacity: 1 })), + state("removed", style({ opacity: 0 })), + transition("inactive => active", animate("{{ easeTime }}ms {{ easing }}")), + transition("active => removed", animate("{{ easeTime }}ms {{ easing }}")), + ]), + ], + preserveWhitespaces: false, +}) +export class BitwardenToastrComponent extends BaseToastrComponent {} diff --git a/libs/components/src/toast/toastr.css b/libs/components/src/toast/toastr.css new file mode 100644 index 0000000000..fabf8caf10 --- /dev/null +++ b/libs/components/src/toast/toastr.css @@ -0,0 +1,23 @@ +@import "~ngx-toastr/toastr"; +@import "./toast.tokens.css"; + +/* Override all default styles from `ngx-toaster` */ +.toast-container .ngx-toastr { + all: unset; + display: block; + width: var(--bit-toast-width); + + /* Needed to make hover states work in Electron, since the toast appears in the draggable region. */ + -webkit-app-region: no-drag; +} + +/* Disable hover styles */ +.toast-container .ngx-toastr:hover { + box-shadow: none; +} + +.toast-container.toast-bottom-full-width .ngx-toastr { + width: var(--bit-toast-width-full); + margin-left: auto; + margin-right: auto; +} diff --git a/libs/components/src/toast/utils.ts b/libs/components/src/toast/utils.ts new file mode 100644 index 0000000000..4c8323f396 --- /dev/null +++ b/libs/components/src/toast/utils.ts @@ -0,0 +1,14 @@ +/** + * Given a toast message, calculate the ideal timeout length following: + * a minimum of 5 seconds + 1 extra second per 120 additional words + * + * @param message the toast message to be displayed + * @returns the timeout length in milliseconds + */ +export const calculateToastTimeout = (message: string | string[]): number => { + const paragraphs = Array.isArray(message) ? message : [message]; + const numWords = paragraphs + .map((paragraph) => paragraph.split(/\s+/).filter((word) => word !== "")) + .flat().length; + return 5000 + Math.floor(numWords / 120) * 1000; +}; diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 0087af28ae..00ab2ff717 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -17,7 +17,11 @@ --color-background-alt3: 18 82 163; --color-background-alt4: 13 60 119; + /* Can only be used behind the extension refresh flag */ + --color-primary-100: 200 217 249; --color-primary-300: 103 149 232; + /* Can only be used behind the extension refresh flag */ + --color-primary-500: 23 93 220; --color-primary-600: 23 93 220; --color-primary-700: 18 82 163; @@ -43,6 +47,7 @@ --color-text-contrast: 255 255 255; --color-text-alt2: 255 255 255; --color-text-code: 192 17 118; + --color-text-headers: 2 15 102; --tw-ring-offset-color: #ffffff; } @@ -60,7 +65,9 @@ --color-background-alt3: 47 52 61; --color-background-alt4: 16 18 21; + --color-primary-100: 8 31 73; --color-primary-300: 23 93 220; + --color-primary-500: 54 117 232; --color-primary-600: 106 153 240; --color-primary-700: 180 204 249; @@ -86,6 +93,7 @@ --color-text-contrast: 25 30 38; --color-text-alt2: 255 255 255; --color-text-code: 240 141 199; + --color-text-headers: 226 227 228; --tw-ring-offset-color: #1f242e; } @@ -171,6 +179,9 @@ @import "./popover/popover.component.css"; @import "./search/search.component.css"; +@import "./toast/toast.tokens.css"; +@import "./toast/toastr.css"; + /** * tw-break-words does not work with table cells: * https://github.com/tailwindlabs/tailwindcss/issues/835 diff --git a/libs/components/tailwind.config.base.js b/libs/components/tailwind.config.base.js index b76f25eae7..12af316b38 100644 --- a/libs/components/tailwind.config.base.js +++ b/libs/components/tailwind.config.base.js @@ -24,7 +24,11 @@ module.exports = { current: colors.current, black: colors.black, primary: { + // Can only be used behind the extension refresh flag + 100: rgba("--color-primary-100"), 300: rgba("--color-primary-300"), + // Can only be used behind the extension refresh flag + 500: rgba("--color-primary-500"), 600: rgba("--color-primary-600"), 700: rgba("--color-primary-700"), }, @@ -69,6 +73,7 @@ module.exports = { main: rgba("--color-text-main"), muted: rgba("--color-text-muted"), contrast: rgba("--color-text-contrast"), + headers: rgba("--color-text-headers"), alt2: rgba("--color-text-alt2"), code: rgba("--color-text-code"), success: rgba("--color-success-600"), diff --git a/libs/components/tailwind.config.js b/libs/components/tailwind.config.js index 987b969e8f..7a53c82ec5 100644 --- a/libs/components/tailwind.config.js +++ b/libs/components/tailwind.config.js @@ -5,6 +5,7 @@ config.content = [ "libs/components/src/**/*.{html,ts,mdx}", "libs/auth/src/**/*.{html,ts,mdx}", "apps/web/src/**/*.{html,ts,mdx}", + "apps/browser/src/**/*.{html,ts,mdx}", "bitwarden_license/bit-web/src/**/*.{html,ts,mdx}", ".storybook/preview.tsx", ]; diff --git a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts index 9e047b063c..8f9e1abaf1 100644 --- a/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts +++ b/libs/importer/src/importers/bitwarden/bitwarden-password-protected-importer.ts @@ -1,4 +1,8 @@ -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { + Argon2KdfConfig, + KdfConfig, + PBKDF2KdfConfig, +} from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { KdfType } from "@bitwarden/common/platform/enums"; @@ -69,12 +73,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im return false; } - this.key = await this.cryptoService.makePinKey( - password, - jdoc.salt, - jdoc.kdfType, - new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism), - ); + const kdfConfig: KdfConfig = + jdoc.kdfType === KdfType.PBKDF2_SHA256 + ? new PBKDF2KdfConfig(jdoc.kdfIterations) + : new Argon2KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism); + + this.key = await this.cryptoService.makePinKey(password, jdoc.salt, kdfConfig); const encKeyValidation = new EncString(jdoc.encKeyValidation_DO_NOT_EDIT); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts index 1865c94c7d..dd5a210bf8 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/base-vault-export.service.ts @@ -1,7 +1,7 @@ +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { KdfType } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -12,15 +12,14 @@ export class BaseVaultExportService { constructor( protected cryptoService: CryptoService, private cryptoFunctionService: CryptoFunctionService, - private stateService: StateService, + private kdfConfigService: KdfConfigService, ) {} protected async buildPasswordExport(clearText: string, password: string): Promise { - const kdfType: KdfType = await this.stateService.getKdfType(); - const kdfConfig: KdfConfig = await this.stateService.getKdfConfig(); + const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig(); const salt = Utils.fromBufferToB64(await this.cryptoFunctionService.randomBytes(16)); - const key = await this.cryptoService.makePinKey(password, salt, kdfType, kdfConfig); + const key = await this.cryptoService.makePinKey(password, salt, kdfConfig); const encKeyValidation = await this.cryptoService.encrypt(Utils.newGuid(), key); const encText = await this.cryptoService.encrypt(clearText, key); @@ -29,14 +28,17 @@ export class BaseVaultExportService { encrypted: true, passwordProtected: true, salt: salt, - kdfType: kdfType, + kdfType: kdfConfig.kdfType, kdfIterations: kdfConfig.iterations, - kdfMemory: kdfConfig.memory, - kdfParallelism: kdfConfig.parallelism, encKeyValidation_DO_NOT_EDIT: encKeyValidation.encryptedString, data: encText.encryptedString, }; + if (kdfConfig.kdfType === KdfType.Argon2id) { + jsonDoc.kdfMemory = kdfConfig.memory; + jsonDoc.kdfParallelism = kdfConfig.parallelism; + } + return JSON.stringify(jsonDoc, null, " "); } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.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"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 5f3bd9de52..ee178767f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,9 +1,9 @@ import * as papa from "papaparse"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -32,9 +32,9 @@ export class IndividualVaultExportService private cipherService: CipherService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getExport(format: ExportFormat = "csv"): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 2346545231..98baf5dca3 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,10 +1,10 @@ import * as papa from "papaparse"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -36,10 +36,10 @@ export class OrganizationVaultExportService private apiService: ApiService, cryptoService: CryptoService, cryptoFunctionService: CryptoFunctionService, - stateService: StateService, private collectionService: CollectionService, + kdfConfigService: KdfConfigService, ) { - super(cryptoService, cryptoFunctionService, stateService); + super(cryptoService, cryptoFunctionService, kdfConfigService); } async getPasswordProtectedExport( diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index fc8faa4b5b..b30384f9f4 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,13 +1,12 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; +import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; +import { DEFAULT_KDF_CONFIG, KdfType, PBKDF2_ITERATIONS } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { StateService } from "@bitwarden/common/platform/services/state.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"; @@ -110,10 +109,10 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string expect(actual).toEqual(JSON.stringify(items)); } -function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) { +function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { const actual = JSON.stringify(JSON.parse(jsonResult).folders); const folders: FolderResponse[] = []; - folderviews.forEach((c) => { + folderViews.forEach((c) => { const folder = new FolderResponse(); folder.id = c.id; folder.name = c.name.toString(); @@ -144,19 +143,18 @@ describe("VaultExportService", () => { let cipherService: MockProxy; let folderService: MockProxy; let cryptoService: MockProxy; - let stateService: MockProxy; + let kdfConfigService: MockProxy; beforeEach(() => { cryptoFunctionService = mock(); cipherService = mock(); folderService = mock(); cryptoService = mock(); - stateService = mock(); + kdfConfigService = mock(); folderService.getAllDecryptedFromState.mockResolvedValue(UserFolderViews); folderService.getAllFromState.mockResolvedValue(UserFolders); - stateService.getKdfType.mockResolvedValue(KdfType.PBKDF2_SHA256); - stateService.getKdfConfig.mockResolvedValue(new KdfConfig(PBKDF2_ITERATIONS.defaultValue)); + kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); cryptoService.encrypt.mockResolvedValue(new EncString("encrypted")); exportService = new IndividualVaultExportService( @@ -164,7 +162,7 @@ describe("VaultExportService", () => { cipherService, cryptoService, cryptoFunctionService, - stateService, + kdfConfigService, ); }); diff --git a/package-lock.json b/package-lock.json index d72ba9cb19..f853763efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.16", + "tldts": "6.1.18", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -111,16 +111,16 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", - "@types/react": "16.14.57", + "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.4.0", - "@typescript-eslint/parser": "7.4.0", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "@webcomponents/custom-elements": "1.6.0", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", @@ -152,7 +152,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.5", + "jest-mock-extended": "3.0.6", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", @@ -161,10 +161,10 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.13", + "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", @@ -172,7 +172,7 @@ "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", - "tailwindcss": "3.4.1", + "tailwindcss": "3.4.3", "ts-jest": "29.1.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", @@ -193,11 +193,11 @@ }, "apps/browser": { "name": "@bitwarden/browser", - "version": "2024.4.1" + "version": "2024.5.0" }, "apps/cli": { "name": "@bitwarden/cli", - "version": "2024.3.1", + "version": "2024.5.0", "license": "GPL-3.0-only", "dependencies": { "@koa/multer": "3.0.2", @@ -224,7 +224,7 @@ "papaparse": "5.4.1", "proper-lockfile": "4.1.2", "rxjs": "7.8.1", - "tldts": "6.1.16", + "tldts": "6.1.18", "zxcvbn": "4.4.2" }, "bin": { @@ -233,7 +233,7 @@ }, "apps/desktop": { "name": "@bitwarden/desktop", - "version": "2024.4.2", + "version": "2024.5.0", "hasInstallScript": true, "license": "GPL-3.0" }, @@ -247,7 +247,7 @@ }, "apps/web": { "name": "@bitwarden/web-vault", - "version": "2024.4.1" + "version": "2024.5.0" }, "libs/admin-console": { "name": "@bitwarden/admin-console", @@ -10544,9 +10544,9 @@ } }, "node_modules/@types/node-ipc": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.0.tgz", - "integrity": "sha512-0v1oucUgINvWPhknecSBE5xkz74sVgeZgiL/LkWXNTSzFaGspEToA4oR56hjza0Jkk6DsS2EiNU3M2R2KQza9A==", + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz", + "integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -10618,13 +10618,13 @@ "dev": true }, "node_modules/@types/react": { - "version": "16.14.57", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.57.tgz", - "integrity": "sha512-fuNq/GV1a6GgqSuVuC457vYeTbm4E1CUBQVZwSPxqYnRhIzSXCJ1gGqyv+PKhqLyfbKCga9dXHJDzv+4XE41fw==", + "version": "16.14.60", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.60.tgz", + "integrity": "sha512-wIFmnczGsTcgwCBeIYOuy2mdXEiKZ5znU/jNOnMZPQyCcIxauMGWlX0TNG4lZ7NxRKj7YUIZRneJQSSdB2jKgg==", "dev": true, "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", + "@types/scheduler": "^0.16", "csstype": "^3.0.2" } }, @@ -10812,22 +10812,22 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz", - "integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.4.0", - "@typescript-eslint/type-utils": "7.4.0", - "@typescript-eslint/utils": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.4", + "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10847,15 +10847,15 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz", - "integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.4.0", - "@typescript-eslint/utils": "7.4.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10874,18 +10874,18 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz", - "integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.4.0", - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/typescript-estree": "7.4.0", - "semver": "^7.5.4" + "@types/json-schema": "^7.0.15", + "@types/semver": "^7.5.8", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "semver": "^7.6.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -10898,6 +10898,39 @@ "eslint": "^8.56.0" } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/experimental-utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", @@ -10918,15 +10951,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", - "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.4.0", - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/typescript-estree": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { @@ -10946,13 +10979,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", - "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11047,9 +11080,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz", - "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11060,19 +11093,19 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", - "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "@typescript-eslint/visitor-keys": "7.4.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -11087,6 +11120,39 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/@typescript-eslint/utils": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", @@ -11210,13 +11276,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", - "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.4.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "7.7.1", + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -12930,9 +12996,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -12950,7 +13016,7 @@ ], "dependencies": { "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", @@ -24335,12 +24401,12 @@ } }, "node_modules/jest-mock-extended": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.5.tgz", - "integrity": "sha512-/eHdaNPUAXe7f65gHH5urc8SbRVWjYxBqmCgax2uqOBJy8UUcCBMN1upj1eZ8y/i+IqpyEm4Kq0VKss/GCCTdw==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/jest-mock-extended/-/jest-mock-extended-3.0.6.tgz", + "integrity": "sha512-DJuEoFzio0loqdX8NIwkbE9dgIXNzaj//pefOQxGkoivohpxbSQeNHCAiXkDNA/fmM4EIJDoZnSibP4w3dUJ9g==", "dev": true, "dependencies": { - "ts-essentials": "^7.0.3" + "ts-essentials": "^9.4.2" }, "peerDependencies": { "jest": "^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 || ^28.0.0 || ^29.0.0", @@ -27864,9 +27930,9 @@ "dev": true }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -31380,9 +31446,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.13.tgz", - "integrity": "sha512-2tPWHCFNC+WRjAC4SIWQNSOdcL1NNkydXim8w7TDqlZi+/ulZYz2OouAI6qMtkggnPt7lGamboj6LcTMwcCvoQ==", + "version": "0.5.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.5.14.tgz", + "integrity": "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q==", "dev": true, "engines": { "node": ">=14.21.3" @@ -32019,9 +32085,9 @@ } }, "node_modules/react": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "dev": true, "dependencies": { "loose-envify": "^1.1.0" @@ -32041,16 +32107,16 @@ } }, "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "dev": true, "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" + "scheduler": "^0.23.2" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.3.1" } }, "node_modules/react-is": { @@ -33538,9 +33604,9 @@ } }, "node_modules/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "dev": true, "dependencies": { "loose-envify": "^1.1.0" @@ -35335,9 +35401,9 @@ "dev": true }, "node_modules/tailwindcss": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.1.tgz", - "integrity": "sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", "dev": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -35348,7 +35414,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -35990,20 +36056,20 @@ "dev": true }, "node_modules/tldts": { - "version": "6.1.16", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.16.tgz", - "integrity": "sha512-X6VrQzW4RymhI1kBRvrWzYlRLXTftZpi7/s/9ZlDILA04yM2lNX7mBvkzDib9L4uSymHt8mBbeaielZMdsAkfQ==", + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.18.tgz", + "integrity": "sha512-F+6zjPFnFxZ0h6uGb8neQWwHQm8u3orZVFribsGq4eBgEVrzSkHxzWS2l6aKr19T1vXiOMFjqfff4fQt+WgJFg==", "dependencies": { - "tldts-core": "^6.1.16" + "tldts-core": "^6.1.18" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.16", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.16.tgz", - "integrity": "sha512-rxnuCux+zn3hMF57nBzr1m1qGZH7Od2ErbDZjVm04fk76cEynTg3zqvHjx5BsBl8lvRTjpzIhsEGMHDH/Hr2Vw==" + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.18.tgz", + "integrity": "sha512-e4wx32F/7dMBSZyKAx825Yte3U0PQtZZ0bkWxYQiwLteRVnQ5zM40fEbi0IyNtwQssgJAk3GCr7Q+w39hX0VKA==" }, "node_modules/tmp": { "version": "0.0.33", @@ -36278,12 +36344,17 @@ } }, "node_modules/ts-essentials": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", - "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-9.4.2.tgz", + "integrity": "sha512-mB/cDhOvD7pg3YCLk2rOtejHjjdSi9in/IBYE13S+8WA5FBSraYf4V/ws55uvs0IvQ/l0wBOlXy5yBNZ9Bl8ZQ==", "dev": true, "peerDependencies": { - "typescript": ">=3.7.0" + "typescript": ">=4.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/ts-interface-checker": { diff --git a/package.json b/package.json index 057e737903..5aca97a6f6 100644 --- a/package.json +++ b/package.json @@ -72,16 +72,16 @@ "@types/node": "18.19.29", "@types/node-fetch": "2.6.4", "@types/node-forge": "1.3.11", - "@types/node-ipc": "9.2.0", + "@types/node-ipc": "9.2.3", "@types/papaparse": "5.3.14", "@types/proper-lockfile": "4.1.4", - "@types/react": "16.14.57", + "@types/react": "16.14.60", "@types/retry": "0.12.5", "@types/zxcvbn": "4.4.4", - "@typescript-eslint/eslint-plugin": "7.4.0", - "@typescript-eslint/parser": "7.4.0", + "@typescript-eslint/eslint-plugin": "7.7.1", + "@typescript-eslint/parser": "7.7.1", "@webcomponents/custom-elements": "1.6.0", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "base64-loader": "1.0.0", "chromatic": "10.9.6", "concurrently": "8.2.2", @@ -113,7 +113,7 @@ "html-webpack-plugin": "5.6.0", "husky": "9.0.11", "jest-junit": "16.0.0", - "jest-mock-extended": "3.0.5", + "jest-mock-extended": "3.0.6", "jest-preset-angular": "14.0.3", "lint-staged": "15.2.2", "mini-css-extract-plugin": "2.8.1", @@ -122,10 +122,10 @@ "postcss": "8.4.38", "postcss-loader": "8.1.1", "prettier": "3.2.2", - "prettier-plugin-tailwindcss": "0.5.13", + "prettier-plugin-tailwindcss": "0.5.14", "process": "0.11.10", - "react": "18.2.0", - "react-dom": "18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", "regedit": "^3.0.3", "remark-gfm": "3.0.1", "rimraf": "5.0.5", @@ -133,7 +133,7 @@ "sass-loader": "13.3.3", "storybook": "7.6.17", "style-loader": "3.3.4", - "tailwindcss": "3.4.1", + "tailwindcss": "3.4.3", "ts-jest": "29.1.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", @@ -200,7 +200,7 @@ "qrious": "4.0.2", "rxjs": "7.8.1", "tabbable": "6.2.0", - "tldts": "6.1.16", + "tldts": "6.1.18", "utf-8-validate": "6.0.3", "zone.js": "0.13.3", "zxcvbn": "4.4.2" @@ -213,7 +213,7 @@ "replacestream": "4.0.3" }, "resolutions": { - "@types/react": "16.14.57" + "@types/react": "16.14.60" }, "lint-staged": { "*": "prettier --cache --ignore-unknown --write", diff --git a/tsconfig.json b/tsconfig.json index ab3f8861a9..60dc9d223e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "noImplicitAny": true, "target": "ES2016", "module": "ES2020", - "lib": ["es5", "es6", "es7", "dom"], + "lib": ["es5", "es6", "es7", "dom", "ES2021"], "sourceMap": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, @@ -38,6 +38,16 @@ ], "useDefineForClassFields": false }, - "include": ["apps/web/src/**/*", "libs/*/src/**/*", "bitwarden_license/bit-web/src/**/*"], - "exclude": ["apps/web/src/**/*.spec.ts", "libs/*/src/**/*.spec.ts", "**/*.spec-util.ts"] + "include": [ + "apps/web/src/**/*", + "apps/browser/src/**/*", + "libs/*/src/**/*", + "bitwarden_license/bit-web/src/**/*" + ], + "exclude": [ + "apps/web/src/**/*.spec.ts", + "apps/browser/src/**/*.spec.ts", + "libs/*/src/**/*.spec.ts", + "**/*.spec-util.ts" + ] }
{{ "name" | i18n }}{{ "owner" | i18n }}
diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index f1446c4209..bcace60ac0 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { ComponentFixture, TestBed } from "@angular/core/testing"; import { mock, MockProxy } from "jest-mock-extended"; +import { of } from "rxjs"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -8,6 +9,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/vault"; import { cipherData } from "./reports-ciphers.mock"; @@ -17,9 +19,14 @@ describe("WeakPasswordsReportComponent", () => { let component: WeakPasswordsReportComponent; let fixture: ComponentFixture; let passwordStrengthService: MockProxy; + let organizationService: MockProxy; + let syncServiceMock: MockProxy; beforeEach(() => { + syncServiceMock = mock(); passwordStrengthService = mock(); + organizationService = mock(); + organizationService.organizations$ = of([]); // 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 TestBed.configureTestingModule({ @@ -35,7 +42,7 @@ describe("WeakPasswordsReportComponent", () => { }, { provide: OrganizationService, - useValue: mock(), + useValue: organizationService, }, { provide: ModalService, @@ -45,6 +52,10 @@ describe("WeakPasswordsReportComponent", () => { provide: PasswordRepromptService, useValue: mock(), }, + { + provide: SyncService, + useValue: syncServiceMock, + }, { provide: I18nService, useValue: mock(), @@ -81,4 +92,8 @@ describe("WeakPasswordsReportComponent", () => { expect(component.ciphers[1].id).toEqual(expectedIdTwo); expect(component.ciphers[1].edit).toEqual(true); }); + + it("should call fullSync method of syncService", () => { + expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false); + }); }); diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts index a7ed119e19..f33e0626ab 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.ts @@ -2,9 +2,11 @@ import { Component, OnInit } from "@angular/core"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { BadgeVariant } from "@bitwarden/components"; @@ -29,8 +31,17 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen protected organizationService: OrganizationService, modalService: ModalService, passwordRepromptService: PasswordRepromptService, + i18nService: I18nService, + syncService: SyncService, ) { - super(modalService, passwordRepromptService, organizationService); + super( + cipherService, + modalService, + passwordRepromptService, + organizationService, + i18nService, + syncService, + ); } async ngOnInit() { @@ -38,7 +49,10 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen } async setCiphers() { - const allCiphers = await this.getAllCiphers(); + const allCiphers: any = await this.getAllCiphers(); + this.passwordStrengthCache = new Map(); + this.weakPasswordCiphers = []; + this.filterStatus = [0]; this.findWeakPasswords(allCiphers); } @@ -55,6 +69,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen ) { return; } + const hasUserName = this.isUserNameNotEmpty(ciph); const cacheKey = this.getCacheKey(ciph); if (!this.passwordStrengthCache.has(cacheKey)) { @@ -87,6 +102,7 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.set(cacheKey, result.score); } const score = this.passwordStrengthCache.get(cacheKey); + if (score != null && score <= 2) { this.passwordStrengthMap.set(id, this.scoreKey(score)); this.weakPasswordCiphers.push(ciph); @@ -98,11 +114,8 @@ export class WeakPasswordsReportComponent extends CipherReportComponent implemen this.passwordStrengthCache.get(this.getCacheKey(b)) ); }); - this.ciphers = [...this.weakPasswordCiphers]; - } - protected getAllCiphers(): Promise { - return this.cipherService.getAllDecrypted(); + this.filterCiphersByOrg(this.weakPasswordCiphers); } protected canManageCipher(c: CipherView): boolean { diff --git a/apps/web/src/app/tools/send/add-edit.component.ts b/apps/web/src/app/tools/send/add-edit.component.ts index ee4be41488..cca416db9c 100644 --- a/apps/web/src/app/tools/send/add-edit.component.ts +++ b/apps/web/src/app/tools/send/add-edit.component.ts @@ -5,6 +5,7 @@ import { FormBuilder } from "@angular/forms"; import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/tools/send/add-edit.component"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -40,6 +41,7 @@ export class AddEditComponent extends BaseAddEditComponent { billingAccountProfileStateService: BillingAccountProfileStateService, protected dialogRef: DialogRef, @Inject(DIALOG_DATA) params: { sendId: string }, + accountService: AccountService, ) { super( i18nService, @@ -55,6 +57,7 @@ export class AddEditComponent extends BaseAddEditComponent { dialogService, formBuilder, billingAccountProfileStateService, + accountService, ); this.sendId = params.sendId; diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 8e0d610c93..4e95bb4bcc 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -76,7 +76,7 @@ export enum CollectionDialogAction { }) export class CollectionDialogComponent implements OnInit, OnDestroy { protected flexibleCollectionsV1Enabled$ = this.configService - .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1, false) + .getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1) .pipe(first()); private destroy$ = new Subject(); 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 5433dd9981..c61bbb46a5 100644 --- a/apps/web/src/app/vault/components/premium-badge.stories.ts +++ b/apps/web/src/app/vault/components/premium-badge.stories.ts @@ -4,15 +4,15 @@ import { of } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { MessageSender } from "@bitwarden/common/platform/messaging"; import { BadgeModule, I18nMockService } from "@bitwarden/components"; import { PremiumBadgeComponent } from "./premium-badge.component"; -class MockMessagingService implements MessagingService { - send(subscriber: string, arg?: any) { +class MockMessagingService implements MessageSender { + send = () => { alert("Clicked on badge"); - } + }; } export default { @@ -31,7 +31,7 @@ export default { }, }, { - provide: MessagingService, + provide: MessageSender, useFactory: () => { return new MockMessagingService(); }, 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 5ddabf0557..ae22d89f7f 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 @@ -6,6 +6,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'vaultItemSelect' | i18n" /> diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html index a6d7854267..897d360b4b 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.html @@ -7,6 +7,7 @@ [disabled]="disabled" [checked]="checked" (change)="$event ? this.checkedToggled.next() : null" + [attr.aria-label]="'collectionItemSelect' | i18n" /> @@ -19,7 +20,7 @@ bitLink [disabled]="disabled" type="button" - class="tw-w-full tw-truncate tw-text-start tw-leading-snug" + class="tw-flex tw-w-full tw-text-start tw-leading-snug" linkType="secondary" title="{{ 'viewCollectionWithName' | i18n: collection.name }}" [routerLink]="[]" @@ -27,7 +28,15 @@ queryParamsHandling="merge" appStopProp > - {{ collection.name }} + {{ collection.name }} +
+ {{ "addAccess" | i18n }} +
@@ -48,7 +57,7 @@ > -

+

{{ permissionText }}

- + + + {{ item.labelName }} {{ item.labelName }} + + +
+ {{ item.permission | i18n }} +
+
+
{{ staticPermission | i18n }}
diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts index 15449f0416..e8e17fd83b 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.component.ts @@ -35,6 +35,34 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn private notifyOnTouch: () => void; private pauseChangeNotification: boolean; + /** + * Updates the enabled/disabled state of provided row form group based on the item's readonly state. + * If a row is enabled, it also updates the enabled/disabled state of the permission control + * based on the item's accessAllItems state and the current value of `permissionMode`. + * @param controlRow - The form group for the row to update + * @param item - The access item that is represented by the row + */ + private updateRowControlDisableState = ( + controlRow: FormGroup>, + item: ApItemViewType, + ) => { + // Disable entire row form group if readOnly + if (item.readOnly || this.disabled) { + controlRow.disable(); + } else { + controlRow.enable(); + } + }; + + /** + * Updates the enabled/disabled state of ALL row form groups based on each item's readonly state. + */ + private updateAllRowControlDisableStates = () => { + this.selectionList.forEachControlItem((controlRow, item) => { + this.updateRowControlDisableState(controlRow as FormGroup>, item); + }); + }; + /** * The internal selection list that tracks the value of this form control / component. * It's responsible for keeping items sorted and synced with the rendered form controls @@ -59,6 +87,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn currentUserInGroup: new FormControl(currentUserInGroup), currentUser: new FormControl(currentUser), }); + + this.updateRowControlDisableState(fg, item); + return fg; }, this._itemComparator.bind(this)); @@ -100,7 +131,13 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn set items(val: ApItemViewType[]) { if (val != null) { - const selected = this.selectionList.formArray.getRawValue() ?? []; + let selected = this.selectionList.formArray.getRawValue() ?? []; + selected = selected.concat( + val + .filter((m) => m.readOnly) + .map((m) => ({ id: m.id, type: m.type, permission: m.permission })), + ); + this.selectionList.populateItems( val.map((m) => { m.icon = m.icon ?? ApItemEnumUtil.itemIcon(m.type); @@ -137,6 +174,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn } else { this.formGroup.enable(); this.multiSelectFormGroup.enable(); + // The enable() above automatically enables all the row controls, + // so we need to disable the readonly ones again + this.updateAllRowControlDisableStates(); } } @@ -149,6 +189,9 @@ export class AccessPolicySelectorComponent implements ControlValueAccessor, OnIn // Always clear the internal selection list on a new value this.selectionList.deselectAll(); + // We need to also select any read only items to appear in the table + this.selectionList.selectItems(this.items.filter((m) => m.readOnly).map((m) => m.id)); + // If the new value is null, then we're done if (selectedItems == null) { this.pauseChangeNotification = false; diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts index 8c0f9c3731..324406e766 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/access-policy-selector.service.spec.ts @@ -347,6 +347,7 @@ function createApItemViewType(options: Partial = {}) { labelName: options?.labelName ?? "test", type: options?.type ?? ApItemEnum.User, permission: options?.permission ?? ApPermissionEnum.CanRead, + readOnly: options?.readOnly ?? false, }; } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts index 362f3c524a..935c77f1b3 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-value.type.ts @@ -1,11 +1,17 @@ import { - ProjectPeopleAccessPoliciesView, UserProjectAccessPolicyView, GroupProjectAccessPolicyView, - ServiceAccountPeopleAccessPoliciesView, UserServiceAccountAccessPolicyView, GroupServiceAccountAccessPolicyView, -} from "../../../../models/view/access-policy.view"; + ServiceAccountProjectAccessPolicyView, +} from "../../../../models/view/access-policies/access-policy.view"; +import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { + ServiceAccountGrantedPoliciesView, + ServiceAccountProjectPolicyPermissionDetailsView, +} from "../../../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; import { ApItemEnum } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; @@ -76,3 +82,46 @@ export function convertToServiceAccountPeopleAccessPoliciesView( }); return view; } + +export function convertToServiceAccountGrantedPoliciesView( + serviceAccountId: string, + selectedPolicyValues: ApItemValueType[], +): ServiceAccountGrantedPoliciesView { + const view = new ServiceAccountGrantedPoliciesView(); + + view.grantedProjectPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.Project) + .map((filtered) => { + const detailView = new ServiceAccountProjectPolicyPermissionDetailsView(); + const policyView = new ServiceAccountProjectAccessPolicyView(); + policyView.serviceAccountId = serviceAccountId; + policyView.grantedProjectId = filtered.id; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + + detailView.accessPolicy = policyView; + return detailView; + }); + + return view; +} + +export function convertToProjectServiceAccountsAccessPoliciesView( + projectId: string, + selectedPolicyValues: ApItemValueType[], +): ProjectServiceAccountsAccessPoliciesView { + const view = new ProjectServiceAccountsAccessPoliciesView(); + + view.serviceAccountAccessPolicies = selectedPolicyValues + .filter((x) => x.type == ApItemEnum.ServiceAccount) + .map((filtered) => { + const policyView = new ServiceAccountProjectAccessPolicyView(); + policyView.serviceAccountId = filtered.id; + policyView.grantedProjectId = projectId; + policyView.read = ApPermissionEnumUtil.toRead(filtered.permission); + policyView.write = ApPermissionEnumUtil.toWrite(filtered.permission); + return policyView; + }); + + return view; +} diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts index 1f494b8fbf..1a023659c1 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy-selector/models/ap-item-view.type.ts @@ -1,11 +1,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SelectItemView } from "@bitwarden/components"; -import { - ProjectPeopleAccessPoliciesView, - ServiceAccountPeopleAccessPoliciesView, -} from "../../../../models/view/access-policy.view"; -import { PotentialGranteeView } from "../../../../models/view/potential-grantee.view"; +import { PotentialGranteeView } from "../../../../models/view/access-policies/potential-grantee.view"; +import { ProjectPeopleAccessPoliciesView } from "../../../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { ServiceAccountGrantedPoliciesView } from "../../../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../../../models/view/access-policies/service-account-people-access-policies.view"; import { ApItemEnum, ApItemEnumUtil } from "./enums/ap-item.enum"; import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.enum"; @@ -13,6 +13,12 @@ import { ApPermissionEnum, ApPermissionEnumUtil } from "./enums/ap-permission.en export type ApItemViewType = SelectItemView & { accessPolicyId?: string; permission?: ApPermissionEnum; + /** + * Flag that this item cannot be modified. + * This will disable the permission editor and will keep + * the item always selected. + */ + readOnly: boolean; } & ( | { type: ApItemEnum.User; @@ -47,6 +53,7 @@ export function convertToAccessPolicyItemViews( permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), userId: policy.userId, currentUser: policy.currentUser, + readOnly: false, }); }); @@ -60,12 +67,59 @@ export function convertToAccessPolicyItemViews( listName: policy.groupName, permission: ApPermissionEnumUtil.toApPermissionEnum(policy.read, policy.write), currentUserInGroup: policy.currentUserInGroup, + readOnly: false, }); }); return accessPolicies; } +export function convertGrantedPoliciesToAccessPolicyItemViews( + value: ServiceAccountGrantedPoliciesView, +): ApItemViewType[] { + const accessPolicies: ApItemViewType[] = []; + + value.grantedProjectPolicies.forEach((detailView) => { + accessPolicies.push({ + type: ApItemEnum.Project, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.Project), + id: detailView.accessPolicy.grantedProjectId, + accessPolicyId: detailView.accessPolicy.id, + labelName: detailView.accessPolicy.grantedProjectName, + listName: detailView.accessPolicy.grantedProjectName, + permission: ApPermissionEnumUtil.toApPermissionEnum( + detailView.accessPolicy.read, + detailView.accessPolicy.write, + ), + readOnly: !detailView.hasPermission, + }); + }); + return accessPolicies; +} + +export function convertProjectServiceAccountsViewToApItemViews( + value: ProjectServiceAccountsAccessPoliciesView, +): ApItemViewType[] { + const accessPolicies: ApItemViewType[] = []; + + value.serviceAccountAccessPolicies.forEach((accessPolicyView) => { + accessPolicies.push({ + type: ApItemEnum.ServiceAccount, + icon: ApItemEnumUtil.itemIcon(ApItemEnum.ServiceAccount), + id: accessPolicyView.serviceAccountId, + accessPolicyId: accessPolicyView.id, + labelName: accessPolicyView.serviceAccountName, + listName: accessPolicyView.serviceAccountName, + permission: ApPermissionEnumUtil.toApPermissionEnum( + accessPolicyView.read, + accessPolicyView.write, + ), + readOnly: false, + }); + }); + return accessPolicies; +} + export function convertPotentialGranteesToApItemViewType( grantees: PotentialGranteeView[], ): ApItemViewType[] { @@ -108,6 +162,7 @@ export function convertPotentialGranteesToApItemViewType( listName: listName, currentUserInGroup: granteeView.currentUserInGroup, currentUser: granteeView.currentUser, + readOnly: false, }; }); } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts index 05b95e127d..32c130647a 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-policy.service.ts @@ -1,5 +1,4 @@ import { Injectable } from "@angular/core"; -import { Subject } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ListResponse } from "@bitwarden/common/models/response/list.response"; @@ -9,24 +8,25 @@ import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { - BaseAccessPolicyView, - GroupProjectAccessPolicyView, - GroupServiceAccountAccessPolicyView, - ProjectAccessPoliciesView, - ProjectPeopleAccessPoliciesView, - ServiceAccountProjectAccessPolicyView, UserProjectAccessPolicyView, + GroupProjectAccessPolicyView, UserServiceAccountAccessPolicyView, - ServiceAccountPeopleAccessPoliciesView, -} from "../../models/view/access-policy.view"; -import { PotentialGranteeView } from "../../models/view/potential-grantee.view"; -import { AccessPoliciesCreateRequest } from "../../shared/access-policies/models/requests/access-policies-create.request"; + GroupServiceAccountAccessPolicyView, + ServiceAccountProjectAccessPolicyView, +} from "../../models/view/access-policies/access-policy.view"; +import { PotentialGranteeView } from "../../models/view/access-policies/potential-grantee.view"; +import { ProjectPeopleAccessPoliciesView } from "../../models/view/access-policies/project-people-access-policies.view"; +import { ProjectServiceAccountsAccessPoliciesView } from "../../models/view/access-policies/project-service-accounts-access-policies.view"; +import { + ServiceAccountGrantedPoliciesView, + ServiceAccountProjectPolicyPermissionDetailsView, +} from "../../models/view/access-policies/service-account-granted-policies.view"; +import { ServiceAccountPeopleAccessPoliciesView } from "../../models/view/access-policies/service-account-people-access-policies.view"; import { PeopleAccessPoliciesRequest } from "../../shared/access-policies/models/requests/people-access-policies.request"; -import { ProjectAccessPoliciesResponse } from "../../shared/access-policies/models/responses/project-access-policies.response"; +import { ServiceAccountGrantedPoliciesRequest } from "../access-policies/models/requests/service-account-granted-policies.request"; -import { AccessPolicyUpdateRequest } from "./models/requests/access-policy-update.request"; import { AccessPolicyRequest } from "./models/requests/access-policy.request"; -import { GrantedPolicyRequest } from "./models/requests/granted-policy.request"; +import { ProjectServiceAccountsAccessPoliciesRequest } from "./models/requests/project-service-accounts-access-policies.request"; import { GroupServiceAccountAccessPolicyResponse, UserServiceAccountAccessPolicyResponse, @@ -36,92 +36,21 @@ import { } from "./models/responses/access-policy.response"; import { PotentialGranteeResponse } from "./models/responses/potential-grantee.response"; import { ProjectPeopleAccessPoliciesResponse } from "./models/responses/project-people-access-policies.response"; +import { ProjectServiceAccountsAccessPoliciesResponse } from "./models/responses/project-service-accounts-access-policies.response"; +import { ServiceAccountGrantedPoliciesPermissionDetailsResponse } from "./models/responses/service-account-granted-policies-permission-details.response"; import { ServiceAccountPeopleAccessPoliciesResponse } from "./models/responses/service-account-people-access-policies.response"; +import { ServiceAccountProjectPolicyPermissionDetailsResponse } from "./models/responses/service-account-project-policy-permission-details.response"; @Injectable({ providedIn: "root", }) export class AccessPolicyService { - private _projectAccessPolicyChanges$ = new Subject(); - private _serviceAccountGrantedPolicyChanges$ = new Subject< - ServiceAccountProjectAccessPolicyView[] - >(); - - /** - * Emits when a project access policy is created or deleted. - */ - readonly projectAccessPolicyChanges$ = this._projectAccessPolicyChanges$.asObservable(); - - /** - * Emits when a service account granted policy is created or deleted. - */ - readonly serviceAccountGrantedPolicyChanges$ = - this._serviceAccountGrantedPolicyChanges$.asObservable(); - constructor( private cryptoService: CryptoService, protected apiService: ApiService, protected encryptService: EncryptService, ) {} - refreshProjectAccessPolicyChanges() { - this._projectAccessPolicyChanges$.next(null); - } - - async getGrantedPolicies( - serviceAccountId: string, - organizationId: string, - ): Promise { - const r = await this.apiService.send( - "GET", - "/service-accounts/" + serviceAccountId + "/granted-policies", - null, - true, - true, - ); - - const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse); - return await this.createServiceAccountProjectAccessPolicyViews(results.data, organizationId); - } - - async createGrantedPolicies( - organizationId: string, - serviceAccountId: string, - policies: ServiceAccountProjectAccessPolicyView[], - ): Promise { - const request = this.getGrantedPoliciesCreateRequest(policies); - const r = await this.apiService.send( - "POST", - "/service-accounts/" + serviceAccountId + "/granted-policies", - request, - true, - true, - ); - const results = new ListResponse(r, ServiceAccountProjectAccessPolicyResponse); - const views = await this.createServiceAccountProjectAccessPolicyViews( - results.data, - organizationId, - ); - this._serviceAccountGrantedPolicyChanges$.next(views); - return views; - } - - async getProjectAccessPolicies( - organizationId: string, - projectId: string, - ): Promise { - const r = await this.apiService.send( - "GET", - "/projects/" + projectId + "/access-policies", - null, - true, - true, - ); - - const results = new ProjectAccessPoliciesResponse(r); - return await this.createProjectAccessPoliciesView(organizationId, results); - } - async getProjectPeopleAccessPolicies( projectId: string, ): Promise { @@ -184,62 +113,255 @@ export class AccessPolicyService { return this.createServiceAccountPeopleAccessPoliciesView(results); } - async createProjectAccessPolicies( + async getServiceAccountGrantedPolicies( organizationId: string, - projectId: string, - projectAccessPoliciesView: ProjectAccessPoliciesView, - ): Promise { - const request = this.getAccessPoliciesCreateRequest(projectAccessPoliciesView); + serviceAccountId: string, + ): Promise { const r = await this.apiService.send( - "POST", - "/projects/" + projectId + "/access-policies", + "GET", + "/service-accounts/" + serviceAccountId + "/granted-policies", + null, + true, + true, + ); + + const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r); + return await this.createServiceAccountGrantedPoliciesView(result, organizationId); + } + + async putServiceAccountGrantedPolicies( + organizationId: string, + serviceAccountId: string, + policies: ServiceAccountGrantedPoliciesView, + ): Promise { + const request = this.getServiceAccountGrantedPoliciesRequest(policies); + const r = await this.apiService.send( + "PUT", + "/service-accounts/" + serviceAccountId + "/granted-policies", request, true, true, ); - const results = new ProjectAccessPoliciesResponse(r); - const view = await this.createProjectAccessPoliciesView(organizationId, results); - this._projectAccessPolicyChanges$.next(view); - return view; + + const result = new ServiceAccountGrantedPoliciesPermissionDetailsResponse(r); + return await this.createServiceAccountGrantedPoliciesView(result, organizationId); } - async deleteAccessPolicy(accessPolicyId: string): Promise { - await this.apiService.send("DELETE", "/access-policies/" + accessPolicyId, null, true, false); - this._projectAccessPolicyChanges$.next(null); - this._serviceAccountGrantedPolicyChanges$.next(null); - } - - async updateAccessPolicy(baseAccessPolicyView: BaseAccessPolicyView): Promise { - const payload = new AccessPolicyUpdateRequest(); - payload.read = baseAccessPolicyView.read; - payload.write = baseAccessPolicyView.write; - await this.apiService.send( - "PUT", - "/access-policies/" + baseAccessPolicyView.id, - payload, + async getProjectServiceAccountsAccessPolicies( + organizationId: string, + projectId: string, + ): Promise { + const r = await this.apiService.send( + "GET", + "/projects/" + projectId + "/access-policies/service-accounts", + null, true, true, ); + + const result = new ProjectServiceAccountsAccessPoliciesResponse(r); + return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); } - private async createProjectAccessPoliciesView( + async putProjectServiceAccountsAccessPolicies( organizationId: string, - projectAccessPoliciesResponse: ProjectAccessPoliciesResponse, - ): Promise { - const orgKey = await this.getOrganizationKey(organizationId); - const view = new ProjectAccessPoliciesView(); + projectId: string, + policies: ProjectServiceAccountsAccessPoliciesView, + ): Promise { + const request = this.getProjectServiceAccountsAccessPoliciesRequest(policies); + const r = await this.apiService.send( + "PUT", + "/projects/" + projectId + "/access-policies/service-accounts", + request, + true, + true, + ); - view.userAccessPolicies = projectAccessPoliciesResponse.userAccessPolicies.map((ap) => { - return this.createUserProjectAccessPolicyView(ap); + const result = new ProjectServiceAccountsAccessPoliciesResponse(r); + return await this.createProjectServiceAccountsAccessPoliciesView(result, organizationId); + } + + async getPeoplePotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/people/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + async getServiceAccountsPotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + async getProjectsPotentialGrantees(organizationId: string) { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/access-policies/projects/potential-grantees", + null, + true, + true, + ); + const results = new ListResponse(r, PotentialGranteeResponse); + return await this.createPotentialGranteeViews(organizationId, results.data); + } + + private async getOrganizationKey(organizationId: string): Promise { + return await this.cryptoService.getOrgKey(organizationId); + } + + private getAccessPolicyRequest( + granteeId: string, + view: + | UserProjectAccessPolicyView + | UserServiceAccountAccessPolicyView + | GroupProjectAccessPolicyView + | GroupServiceAccountAccessPolicyView + | ServiceAccountProjectAccessPolicyView, + ) { + const request = new AccessPolicyRequest(); + request.granteeId = granteeId; + request.read = view.read; + request.write = view.write; + return request; + } + + private getServiceAccountGrantedPoliciesRequest( + policies: ServiceAccountGrantedPoliciesView, + ): ServiceAccountGrantedPoliciesRequest { + const request = new ServiceAccountGrantedPoliciesRequest(); + + request.projectGrantedPolicyRequests = policies.grantedProjectPolicies.map((detailView) => ({ + grantedId: detailView.accessPolicy.grantedProjectId, + read: detailView.accessPolicy.read, + write: detailView.accessPolicy.write, + })); + + return request; + } + + private getProjectServiceAccountsAccessPoliciesRequest( + policies: ProjectServiceAccountsAccessPoliciesView, + ): ProjectServiceAccountsAccessPoliciesRequest { + const request = new ProjectServiceAccountsAccessPoliciesRequest(); + + request.serviceAccountAccessPolicyRequests = policies.serviceAccountAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.serviceAccountId, ap); }); - view.groupAccessPolicies = projectAccessPoliciesResponse.groupAccessPolicies.map((ap) => { - return this.createGroupProjectAccessPolicyView(ap); - }); - view.serviceAccountAccessPolicies = await Promise.all( - projectAccessPoliciesResponse.serviceAccountAccessPolicies.map(async (ap) => { - return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); + + return request; + } + + private getPeopleAccessPoliciesRequest( + view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, + ): PeopleAccessPoliciesRequest { + const request = new PeopleAccessPoliciesRequest(); + + if (view.userAccessPolicies?.length > 0) { + request.userAccessPolicyRequests = view.userAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.organizationUserId, ap); + }); + } + + if (view.groupAccessPolicies?.length > 0) { + request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => { + return this.getAccessPolicyRequest(ap.groupId, ap); + }); + } + + return request; + } + + private createBaseAccessPolicyView( + response: + | UserProjectAccessPolicyResponse + | UserServiceAccountAccessPolicyResponse + | GroupProjectAccessPolicyResponse + | GroupServiceAccountAccessPolicyResponse + | ServiceAccountProjectAccessPolicyResponse, + ) { + return { + id: response.id, + read: response.read, + write: response.write, + creationDate: response.creationDate, + revisionDate: response.revisionDate, + }; + } + + private async createPotentialGranteeViews( + organizationId: string, + results: PotentialGranteeResponse[], + ): Promise { + const orgKey = await this.getOrganizationKey(organizationId); + return await Promise.all( + results.map(async (r) => { + const view = new PotentialGranteeView(); + view.id = r.id; + view.type = r.type; + view.email = r.email; + view.currentUser = r.currentUser; + view.currentUserInGroup = r.currentUserInGroup; + + if (r.type === "serviceAccount" || r.type === "project") { + view.name = r.name + ? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey) + : null; + } else { + view.name = r.name; + } + return view; }), ); + } + + private async createServiceAccountGrantedPoliciesView( + response: ServiceAccountGrantedPoliciesPermissionDetailsResponse, + organizationId: string, + ): Promise { + const orgKey = await this.getOrganizationKey(organizationId); + + const view = new ServiceAccountGrantedPoliciesView(); + view.grantedProjectPolicies = + await this.createServiceAccountProjectPolicyPermissionDetailsViews( + orgKey, + response.grantedProjectPolicies, + ); + return view; + } + + private async createServiceAccountProjectPolicyPermissionDetailsViews( + orgKey: SymmetricCryptoKey, + responses: ServiceAccountProjectPolicyPermissionDetailsResponse[], + ): Promise { + return await Promise.all( + responses.map(async (response) => { + return await this.createServiceAccountProjectPolicyPermissionDetailsView(orgKey, response); + }), + ); + } + + private async createServiceAccountProjectPolicyPermissionDetailsView( + orgKey: SymmetricCryptoKey, + response: ServiceAccountProjectPolicyPermissionDetailsResponse, + ): Promise { + const view = new ServiceAccountProjectPolicyPermissionDetailsView(); + view.hasPermission = response.hasPermission; + view.accessPolicy = await this.createServiceAccountProjectAccessPolicyView( + orgKey, + response.accessPolicy, + ); return view; } @@ -271,56 +393,6 @@ export class AccessPolicyService { return view; } - private getAccessPoliciesCreateRequest( - projectAccessPoliciesView: ProjectAccessPoliciesView, - ): AccessPoliciesCreateRequest { - const createRequest = new AccessPoliciesCreateRequest(); - - if (projectAccessPoliciesView.userAccessPolicies?.length > 0) { - createRequest.userAccessPolicyRequests = projectAccessPoliciesView.userAccessPolicies.map( - (ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }, - ); - } - - if (projectAccessPoliciesView.groupAccessPolicies?.length > 0) { - createRequest.groupAccessPolicyRequests = projectAccessPoliciesView.groupAccessPolicies.map( - (ap) => { - return this.getAccessPolicyRequest(ap.groupId, ap); - }, - ); - } - - if (projectAccessPoliciesView.serviceAccountAccessPolicies?.length > 0) { - createRequest.serviceAccountAccessPolicyRequests = - projectAccessPoliciesView.serviceAccountAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.serviceAccountId, ap); - }); - } - return createRequest; - } - - private getPeopleAccessPoliciesRequest( - view: ProjectPeopleAccessPoliciesView | ServiceAccountPeopleAccessPoliciesView, - ): PeopleAccessPoliciesRequest { - const request = new PeopleAccessPoliciesRequest(); - - if (view.userAccessPolicies?.length > 0) { - request.userAccessPolicyRequests = view.userAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.organizationUserId, ap); - }); - } - - if (view.groupAccessPolicies?.length > 0) { - request.groupAccessPolicyRequests = view.groupAccessPolicies.map((ap) => { - return this.getAccessPolicyRequest(ap.groupId, ap); - }); - } - - return request; - } - private createUserProjectAccessPolicyView( response: UserProjectAccessPolicyResponse, ): UserProjectAccessPolicyView { @@ -394,146 +466,18 @@ export class AccessPolicyService { }; } - async getPeoplePotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/people/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - async getServiceAccountsPotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/service-accounts/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - async getProjectsPotentialGrantees(organizationId: string) { - const r = await this.apiService.send( - "GET", - "/organizations/" + organizationId + "/access-policies/projects/potential-grantees", - null, - true, - true, - ); - const results = new ListResponse(r, PotentialGranteeResponse); - return await this.createPotentialGranteeViews(organizationId, results.data); - } - - protected async getOrganizationKey(organizationId: string): Promise { - return await this.cryptoService.getOrgKey(organizationId); - } - - protected getAccessPolicyRequest( - granteeId: string, - view: - | UserProjectAccessPolicyView - | UserServiceAccountAccessPolicyView - | GroupProjectAccessPolicyView - | GroupServiceAccountAccessPolicyView - | ServiceAccountProjectAccessPolicyView, - ) { - const request = new AccessPolicyRequest(); - request.granteeId = granteeId; - request.read = view.read; - request.write = view.write; - return request; - } - - protected createBaseAccessPolicyView( - response: - | UserProjectAccessPolicyResponse - | UserServiceAccountAccessPolicyResponse - | GroupProjectAccessPolicyResponse - | GroupServiceAccountAccessPolicyResponse - | ServiceAccountProjectAccessPolicyResponse, - ) { - return { - id: response.id, - read: response.read, - write: response.write, - creationDate: response.creationDate, - revisionDate: response.revisionDate, - }; - } - - private async createPotentialGranteeViews( + private async createProjectServiceAccountsAccessPoliciesView( + response: ProjectServiceAccountsAccessPoliciesResponse, organizationId: string, - results: PotentialGranteeResponse[], - ): Promise { + ): Promise { const orgKey = await this.getOrganizationKey(organizationId); - return await Promise.all( - results.map(async (r) => { - const view = new PotentialGranteeView(); - view.id = r.id; - view.type = r.type; - view.email = r.email; - view.currentUser = r.currentUser; - view.currentUserInGroup = r.currentUserInGroup; - if (r.type === "serviceAccount" || r.type === "project") { - view.name = r.name - ? await this.encryptService.decryptToUtf8(new EncString(r.name), orgKey) - : null; - } else { - view.name = r.name; - } - return view; - }), - ); - } - - private getGrantedPoliciesCreateRequest( - policies: ServiceAccountProjectAccessPolicyView[], - ): GrantedPolicyRequest[] { - return policies.map((ap) => { - const request = new GrantedPolicyRequest(); - request.grantedId = ap.grantedProjectId; - request.read = ap.read; - request.write = ap.write; - return request; - }); - } - - private async createServiceAccountProjectAccessPolicyViews( - responses: ServiceAccountProjectAccessPolicyResponse[], - organizationId: string, - ): Promise { - const orgKey = await this.getOrganizationKey(organizationId); - return await Promise.all( - responses.map(async (response: ServiceAccountProjectAccessPolicyResponse) => { - const view = new ServiceAccountProjectAccessPolicyView(); - view.id = response.id; - view.read = response.read; - view.write = response.write; - view.creationDate = response.creationDate; - view.revisionDate = response.revisionDate; - view.serviceAccountId = response.serviceAccountId; - view.grantedProjectId = response.grantedProjectId; - view.serviceAccountName = response.serviceAccountName - ? await this.encryptService.decryptToUtf8( - new EncString(response.serviceAccountName), - orgKey, - ) - : null; - view.grantedProjectName = response.grantedProjectName - ? await this.encryptService.decryptToUtf8( - new EncString(response.grantedProjectName), - orgKey, - ) - : null; - return view; + const view = new ProjectServiceAccountsAccessPoliciesView(); + view.serviceAccountAccessPolicies = await Promise.all( + response.serviceAccountAccessPolicies.map(async (ap) => { + return await this.createServiceAccountProjectAccessPolicyView(orgKey, ap); }), ); + return view; } } diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html b/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html deleted file mode 100644 index 9da038a2d8..0000000000 --- a/bitwarden_license/bit-web/src/app/secrets-manager/shared/access-policies/access-selector.component.html +++ /dev/null @@ -1,80 +0,0 @@ -
- - {{ label }} - - {{ hint }} - - -
- - - - -
{{ columnTitle }}{{ "permissions" | i18n }}
- - {{ row.name }} - - - - - {{ "canRead" | i18n }} - {{ "canWrite" | i18n }} - {{ "canReadWrite" | i18n }} - - - -