diff --git a/.eslintrc.json b/.eslintrc.json index 72b65d361d0..cd1a22c5cca 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -324,6 +324,26 @@ "rules": { "no-restricted-imports": ["error", { "patterns": ["@bitwarden/bit-common/*", "src/**/*"] }] } + }, + { + "files": ["apps/**/*.ts"], + "rules": { + // Catches static imports + "no-restricted-imports": [ + "error", + { + "patterns": ["biwarden_license/**", "@bitwarden/bit-common/*", "@bitwarden/bit-web/*"] + } + ], + // Catches dynamic imports, e.g. in routing modules where modules are lazy-loaded + "no-restricted-syntax": [ + "error", + { + "message": "Don't import Bitwarden licensed code into OSS code.", + "selector": "ImportExpression > Literal.source[value=/.*(bitwarden_license|bit-common|bit-web).*/]" + } + ] + } } ] } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 118f9dab277..6b6a905bfdd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,6 +29,7 @@ libs/common/src/models/export @bitwarden/team-tools-dev libs/common/src/tools @bitwarden/team-tools-dev libs/importer @bitwarden/team-tools-dev libs/tools @bitwarden/team-tools-dev +bitwarden_license/bit-web/src/app/tools @bitwarden/team-tools-dev ## Localization/Crowdin (Tools team) apps/browser/src/_locales @bitwarden/team-tools-dev @@ -94,9 +95,10 @@ apps/desktop/src/services/native-message-handler.service.ts @bitwarden/team-auto apps/desktop/src/services/native-messaging.service.ts @bitwarden/team-autofill-dev ## Component Library ## -.storybook @bitwarden/team-component-library -libs/components @bitwarden/team-component-library -apps/web/src/app/layouts/header +.storybook @bitwarden/team-design-system +libs/components @bitwarden/team-design-system +apps/browser/src/platform/popup/layout @bitwarden/team-design-system +apps/web/src/app/layouts @bitwarden/team-design-system ## Desktop native module ## apps/desktop/desktop_native @bitwarden/team-platform-dev diff --git a/.github/renovate.json b/.github/renovate.json index e202e026675..a1200912dc8 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -105,6 +105,26 @@ "commitMessagePrefix": "[deps] Billing:", "reviewers": ["team:team-billing-dev"] }, + { + "matchPackageNames": [ + "@types/argon2-browser", + "@types/chrome", + "@types/firefox-webext-browser", + "@types/jquery", + "@types/node", + "@types/node-forge", + "argon2", + "argon2-browser", + "big-integer", + "node-forge", + "rxjs", + "type-fest", + "typescript" + ], + "description": "Platform owned dependencies", + "commitMessagePrefix": "[deps] Platform:", + "reviewers": ["team:team-platform-dev"] + }, { "matchPackageNames": [ "@angular-devkit/build-angular", @@ -119,30 +139,6 @@ "@angular/platform", "@angular/compiler", "@angular/router", - "@types/argon2-browser", - "@types/chrome", - "@types/firefox-webext-browser", - "@types/jquery", - "@types/node", - "@types/node-forge", - "argon2", - "argon2-browser", - "big-integer", - "bootstrap", - "jquery", - "node-forge", - "popper.js", - "rxjs", - "type-fest", - "typescript", - "zone.js" - ], - "description": "Platform owned dependencies", - "commitMessagePrefix": "[deps] Platform:", - "reviewers": ["team:team-platform-dev"] - }, - { - "matchPackageNames": [ "@compodoc/compodoc", "@ng-select/ng-select", "@storybook/addon-a11y", @@ -153,17 +149,21 @@ "@storybook/angular", "@types/react", "autoprefixer", + "bootstrap", "chromatic", + "jquery", "ngx-toastr", + "popper.js", "react", "react-dom", "remark-gfm", "storybook", - "tailwindcss" + "tailwindcss", + "zone.js" ], "description": "Component library owned dependencies", - "commitMessagePrefix": "[deps] Platform (CL):", - "reviewers": ["team:team-component-library"] + "commitMessagePrefix": "[deps] Design System:", + "reviewers": ["team:team-design-system"] }, { "matchPackageNames": [ diff --git a/.github/whitelist-capital-letters.txt b/.github/whitelist-capital-letters.txt index 89395888365..a825ccbf790 100644 --- a/.github/whitelist-capital-letters.txt +++ b/.github/whitelist-capital-letters.txt @@ -8,6 +8,7 @@ ./libs/admin-console/README.md ./libs/auth/README.md ./libs/billing/README.md +./libs/common/src/tools/integration/README.md ./libs/platform/README.md ./libs/tools/README.md ./libs/tools/export/vault-export/README.md @@ -28,8 +29,6 @@ ./apps/desktop/resources/appx/Wide310x150Logo.png ./apps/desktop/resources/appx/Square44x44Logo.png ./apps/desktop/README.md -./apps/desktop/desktop_native/Cargo.toml -./apps/desktop/desktop_native/Cargo.lock ./apps/cli/stores/chocolatey/tools/VERIFICATION.txt ./apps/cli/README.md ./apps/browser/README.md diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 5e333b3b58a..ad2ac539715 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -71,8 +71,8 @@ jobs: ] license_type: [ - { prefix: "oss", readable: "open source license" }, - { prefix: "bit", readable: "commercial license"} + { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { build_prefix: "bit", artifact_prefix: "", readable: "commercial license"} ] runs-on: ${{ matrix.os.distro }} needs: @@ -104,16 +104,16 @@ jobs: working-directory: ./ - name: Build & Package Unix - run: npm run dist:${{ matrix.license_type.prefix }}:${{ env.SHORT_RUNNER_OS }} --quiet + run: npm run dist:${{ matrix.license_type.build_prefix }}:${{ env.SHORT_RUNNER_OS }} --quiet - name: Zip Unix run: | - cd ./dist/${{ matrix.license_type.prefix }}/${{ env.LOWER_RUNNER_OS }} - zip ../../bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip ./bw + cd ./dist/${{ matrix.license_type.build_prefix }}/${{ env.LOWER_RUNNER_OS }} + zip ../../bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip ./bw - name: Version Test run: | - unzip "./dist/bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip" -d "./test" + unzip "./dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip" -d "./test" testVersion=$(./test/bw -v) echo "version: $_PACKAGE_VERSION" echo "testVersion: $testVersion" @@ -125,21 +125,22 @@ jobs: - name: Create checksums Unix run: | cd ./dist - shasum -a 256 bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip \ - | awk '{split($0, a); print a[1]}' > bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt + shasum -a 256 bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip \ + | awk '{split($0, a); print a[1]}' > bw${{ + matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - name: Upload unix zip asset uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip - path: apps/cli/dist/bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip + name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip + path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload unix checksum asset uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt - path: apps/cli/dist/bw-${{ matrix.license_type.prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt + name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt + path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt if-no-files-found: error cli-windows: @@ -148,8 +149,8 @@ jobs: matrix: license_type: [ - { prefix: "oss", readable: "open source license" }, - { prefix: "bit", readable: "commercial license"} + { build_prefix: "oss", artifact_prefix: "-oss", readable: "open source license" }, + { build_prefix: "bit", artifact_prefix: "", readable: "commercial license"} ] runs-on: windows-2022 needs: @@ -236,26 +237,26 @@ jobs: working-directory: ./ - name: Build & Package Windows - run: npm run dist:${{ matrix.license_type.prefix }}:win --quiet + run: npm run dist:${{ matrix.license_type.build_prefix }}:win --quiet - name: Package Chocolatey shell: pwsh - if: ${{ matrix.license_type.prefix }} == 'oss' + if: ${{ matrix.license_type.build_prefix }} == 'bit' run: | Copy-Item -Path stores/chocolatey -Destination dist/chocolatey -Recurse - Copy-Item dist/${{ matrix.license_type.prefix }}/windows/bw.exe -Destination dist/chocolatey/tools + Copy-Item dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe -Destination dist/chocolatey/tools Copy-Item ${{ github.workspace }}/LICENSE.txt -Destination dist/chocolatey/tools choco pack dist/chocolatey/bitwarden-cli.nuspec --version ${{ env._PACKAGE_VERSION }} --out dist/chocolatey - name: Zip Windows shell: cmd - run: 7z a ./dist/bw-${{ matrix.license_type.prefix }}-windows-%_PACKAGE_VERSION%.zip ./dist/${{ matrix.license_type.prefix }}/windows/bw.exe + run: 7z a ./dist/bw${{ matrix.license_type.artifact_prefix}}-windows-%_PACKAGE_VERSION%.zip ./dist/${{ matrix.license_type.build_prefix }}/windows/bw.exe - name: Version Test run: | dir ./dist/ - Expand-Archive -Path "./dist/bw-${{ matrix.license_type.prefix }}-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/${{ matrix.license_type.prefix }}/windows" - $testVersion = Invoke-Expression '& ./test/${{ matrix.license_type.prefix }}/windows/bw.exe -v' + Expand-Archive -Path "./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${env:_PACKAGE_VERSION}.zip" -DestinationPath "./test/${{ matrix.license_type.build_prefix }}/windows" + $testVersion = Invoke-Expression '& ./test/${{ matrix.license_type.build_prefix }}/windows/bw.exe -v' echo "version: $env:_PACKAGE_VERSION" echo "testVersion: $testVersion" if($testVersion -ne $env:_PACKAGE_VERSION) { @@ -264,25 +265,25 @@ jobs: - name: Create checksums Windows run: | - checksum -f="./dist/bw-${{ matrix.license_type.prefix }}-windows-${env:_PACKAGE_VERSION}.zip" ` - -t sha256 | Out-File -Encoding ASCII ./dist/bw-${{ matrix.license_type.prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt + checksum -f="./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${env:_PACKAGE_VERSION}.zip" ` + -t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt - name: Upload windows zip asset uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: bw-${{ matrix.license_type.prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip - path: apps/cli/dist/bw-${{ matrix.license_type.prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip + name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip + path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip if-no-files-found: error - name: Upload windows checksum asset uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: - name: bw-${{ matrix.license_type.prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt - path: apps/cli/dist/bw-${{ matrix.license_type.prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt + name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt + path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt if-no-files-found: error - name: Upload Chocolatey asset - if: matrix.license_type.prefix == 'oss' + if: matrix.license_type.build_prefix == 'bit' uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg @@ -290,7 +291,7 @@ jobs: if-no-files-found: error - name: Upload NPM Build Directory asset - if: matrix.license_type.prefix == 'oss' + if: matrix.license_type.build_prefix == 'bit' uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip @@ -320,14 +321,9 @@ jobs: - name: Get bw linux cli uses: actions/download-artifact@c850b930e6ba138125429b7e5c93fc707a7f8427 # v4.1.4 with: - name: bw-oss-linux-${{ env._PACKAGE_VERSION }}.zip + name: bw-linux-${{ env._PACKAGE_VERSION }}.zip path: apps/cli/dist/snap - - name: Rename snap artifact - run: | - cd dist/snap - mv bw-oss-linux-${{ env._PACKAGE_VERSION }}.zip bw-linux-${{ env._PACKAGE_VERSION }}.zip - - name: Setup Snap Package run: | cp -r stores/snap/* -t dist/snap diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index bebf7b5646c..9e59bc47853 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -173,14 +173,14 @@ jobs: id: cache with: path: | - apps/desktop/desktop_native/*.node + apps/desktop/desktop_native/napi/*.node ${{ env.RUNNER_TEMP }}/.cargo/registry ${{ env.RUNNER_TEMP }}/.cargo/git key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi env: PKG_CONFIG_ALLOW_CROSS: true PKG_CONFIG_ALL_STATIC: true @@ -301,12 +301,12 @@ jobs: uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 id: cache with: - path: apps/desktop/desktop_native/*.node + path: apps/desktop/desktop_native/napi/*.node key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi run: npm run build:cross-platform - name: Build & Sign (dev) @@ -464,6 +464,9 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Node-gyp + run: python3 -m pip install setuptools + - name: Rust shell: pwsh run: rustup target install aarch64-apple-darwin @@ -581,12 +584,12 @@ jobs: uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 id: cache with: - path: apps/desktop/desktop_native/*.node + path: apps/desktop/desktop_native/napi/*.node key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi run: npm run build:cross-platform - name: Build application (dev) @@ -625,6 +628,9 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Node-gyp + run: python3 -m pip install setuptools + - name: Rust shell: pwsh run: rustup target install aarch64-apple-darwin @@ -742,12 +748,12 @@ jobs: uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 id: cache with: - path: apps/desktop/desktop_native/*.node + path: apps/desktop/desktop_native/napi/*.node key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi run: npm run build:cross-platform - name: Build @@ -831,6 +837,9 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Node-gyp + run: python3 -m pip install setuptools + - name: Rust shell: pwsh run: rustup target install aarch64-apple-darwin @@ -948,12 +957,12 @@ jobs: uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 id: cache with: - path: apps/desktop/desktop_native/*.node + path: apps/desktop/desktop_native/napi/*.node key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi run: npm run build:cross-platform - name: Build @@ -1028,6 +1037,9 @@ jobs: cache-dependency-path: '**/package-lock.json' node-version: ${{ env._NODE_VERSION }} + - name: Set up Node-gyp + run: python3 -m pip install setuptools + - name: Print environment run: | node --version @@ -1140,12 +1152,12 @@ jobs: uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 # v4.0.1 id: cache with: - path: apps/desktop/desktop_native/*.node + path: apps/desktop/desktop_native/napi/*.node key: rust-${{ runner.os }}-${{ hashFiles('apps/desktop/desktop_native/**/*') }} - name: Build Native Module if: steps.cache.outputs.cache-hit != 'true' - working-directory: apps/desktop/desktop_native + working-directory: apps/desktop/desktop_native/napi run: npm run build:cross-platform - name: Build diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml index 1ff67671419..5aa92c4dd8a 100644 --- a/.github/workflows/deploy-web.yml +++ b/.github/workflows/deploy-web.yml @@ -119,6 +119,49 @@ jobs: # Set the sync utility to use for deployment to the environment (az-sync or azcopy) echo "sync-utility=azcopy" >> $GITHUB_OUTPUT + - name: Environment Protection + env: + BUILD_WEB_RUN_ID: ${{ inputs.build-web-run-id }} + GH_TOKEN: ${{ github.token }} + run: | + BRANCH_OR_TAG_LOWER="" + if [[ "$BUILD_WEB_RUN_ID" == "" ]]; then + BRANCH_OR_TAG_LOWER=$(echo ${{ inputs.branch-or-tag }} | awk '{print tolower($0)}') + else + BRANCH_OR_TAG_LOWER=$(gh api /repos/bitwarden/clients/actions/runs/$BUILD_WEB_RUN_ID/artifacts --jq '.artifacts[0].workflow_run.head_branch' | awk '{print tolower($0)}') + fi + + echo "Branch/Tag: $BRANCH_OR_TAG_LOWER" + + PROD_ENV_PATTERN='USPROD|EUPROD' + PROD_ALLOWED_TAGS_PATTERN='web-v[0-9]+\.[0-9]+\.[0-9]+' + + QA_ENV_PATTERN='USQA|EUQA' + QA_ALLOWED_TAGS_PATTERN='.*' + + DEV_ENV_PATTERN='USDEV' + DEV_ALLOWED_TAGS_PATTERN='main' + + if [[ \ + ${{ inputs.environment }} =~ \.*($PROD_ENV_PATTERN)\.* && \ + ! "$BRANCH_OR_TAG_LOWER" =~ ^($PROD_ALLOWED_TAGS_PATTERN).* \ + ]] || [[ \ + ${{ inputs.environment }} =~ \.*($QA_ENV_PATTERN)\.* && \ + ! "$BRANCH_OR_TAG_LOWER" =~ ^($QA_ALLOWED_TAGS_PATTERN).* \ + ]] || [[ \ + ${{ inputs.environment }} =~ \.*($DEV_ENV_PATTERN)\.* && \ + $BRANCH_OR_TAG_LOWER != $DEV_ALLOWED_TAGS_PATTERN \ + ]]; then + echo "!Deployment blocked!" + echo "Attempting to deploy a tag that is not allowed in ${{ inputs.environment }} environment" + echo + echo "Environment: ${{ inputs.environment }}" + echo "Tag: $BRANCH_OR_TAG_LOWER" + exit 1 + else + echo "The input Branch/Tag: '$BRANCH_OR_TAG_LOWER' is allowed to deploy on ${{ inputs.environment }} environment" + fi + approval: name: Approval for Deployment to ${{ needs.setup.outputs.environment-name }} needs: setup diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index ed5ada527c1..2d881a4c304 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -35,6 +35,8 @@ jobs: ! -path "*/.DS_Store" \ ! -path "*/*locales/*" \ ! -path "./.github/*" \ + ! -path "*/Cargo.toml" \ + ! -path "*/Cargo.lock" \ > tmp.txt diff <(sort .github/whitelist-capital-letters.txt) <(sort tmp.txt) diff --git a/.github/workflows/release-cli.yml b/.github/workflows/release-cli.yml index 5b511cd1d95..6d56c3be831 100644 --- a/.github/workflows/release-cli.yml +++ b/.github/workflows/release-cli.yml @@ -102,16 +102,16 @@ jobs: with: artifacts: "apps/cli/bw-oss-windows-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-windows-sha256-${{ env.PKG_VERSION }}.txt, - apps/cli/bw-bit-windows-${{ env.PKG_VERSION }}.zip, - apps/cli/bw-bit-windows-sha256-${{ env.PKG_VERSION }}.txt, + apps/cli/bw-windows-${{ env.PKG_VERSION }}.zip, + apps/cli/bw-windows-sha256-${{ env.PKG_VERSION }}.txt, apps/cli/bw-oss-macos-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-macos-sha256-${{ env.PKG_VERSION }}.txt, - apps/cli/bw-bit-macos-${{ env.PKG_VERSION }}.zip, - apps/cli/bw-bit-macos-sha256-${{ env.PKG_VERSION }}.txt, + apps/cli/bw-macos-${{ env.PKG_VERSION }}.zip, + apps/cli/bw-macos-sha256-${{ env.PKG_VERSION }}.txt, apps/cli/bw-oss-linux-${{ env.PKG_VERSION }}.zip, apps/cli/bw-oss-linux-sha256-${{ env.PKG_VERSION }}.txt, - apps/cli/bw-bit-linux-${{ env.PKG_VERSION }}.zip, - apps/cli/bw-bit-linux-sha256-${{ env.PKG_VERSION }}.txt, + apps/cli/bw-linux-${{ env.PKG_VERSION }}.zip, + apps/cli/bw-linux-sha256-${{ env.PKG_VERSION }}.txt, apps/cli/bitwarden-cli.${{ env.PKG_VERSION }}.nupkg, apps/cli/bw_${{ env.PKG_VERSION }}_amd64.snap, apps/cli/bw-snap-sha256-${{ env.PKG_VERSION }}.txt" diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index dc6957d00d6..eb63a53f2ea 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -29,11 +29,6 @@ on: required: true default: true type: boolean - electron_publish: - description: 'Publish Electron to S3 bucket' - required: true - default: true - type: boolean github_release: description: 'Publish GitHub release' required: true @@ -142,7 +137,6 @@ jobs: run: mv Bitwarden-${{ env.PKG_VERSION }}-universal.pkg Bitwarden-${{ env.PKG_VERSION }}-universal.pkg.archive - name: Set staged rollout percentage - if: ${{ github.event.inputs.electron_publish == 'true' }} env: RELEASE_CHANNEL: ${{ steps.release-channel.outputs.channel }} ROLLOUT_PCT: ${{ inputs.rollout_percentage }} @@ -152,7 +146,7 @@ jobs: echo "stagingPercentage: ${ROLLOUT_PCT}" >> apps/desktop/artifacts/${RELEASE_CHANNEL}-mac.yml - name: Publish artifacts to S3 - if: ${{ github.event.inputs.release_type != 'Dry Run' && github.event.inputs.electron_publish == 'true' }} + if: ${{ github.event.inputs.release_type != 'Dry Run' }} env: AWS_ACCESS_KEY_ID: ${{ steps.retrieve-secrets.outputs.aws-electron-access-id }} AWS_SECRET_ACCESS_KEY: ${{ steps.retrieve-secrets.outputs.aws-electron-access-key }} diff --git a/.github/workflows/staged-rollout-desktop.yml b/.github/workflows/staged-rollout-desktop.yml index a6ca2f1e319..1f751507640 100644 --- a/.github/workflows/staged-rollout-desktop.yml +++ b/.github/workflows/staged-rollout-desktop.yml @@ -41,11 +41,11 @@ jobs: AWS_S3_BUCKET_NAME: ${{ steps.retrieve-secrets.outputs.aws-electron-bucket-name }} run: | aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest.yml . \ - --quiet \ + --quiet aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-linux.yml . \ - --quiet \ + --quiet aws s3 cp $AWS_S3_BUCKET_NAME/desktop/latest-mac.yml . \ - --quiet \ + --quiet - name: Check new rollout percentage env: diff --git a/.storybook/main.ts b/.storybook/main.ts index 26eee201f99..175ed339489 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -6,6 +6,8 @@ const config: StorybookConfig = { stories: [ "../libs/auth/src/**/*.mdx", "../libs/auth/src/**/*.stories.@(js|jsx|ts|tsx)", + "../libs/vault/src/**/*.mdx", + "../libs/vault/src/**/*.stories.@(js|jsx|ts|tsx)", "../libs/components/src/**/*.mdx", "../libs/components/src/**/*.stories.@(js|jsx|ts|tsx)", "../apps/web/src/**/*.mdx", diff --git a/apps/browser/package.json b/apps/browser/package.json index b0a66a37d08..f7c577e7f7f 100644 --- a/apps/browser/package.json +++ b/apps/browser/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/browser", - "version": "2024.6.3", + "version": "2024.7.0", "scripts": { "build": "cross-env MANIFEST_VERSION=3 webpack", "build:mv2": "webpack", diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json index 0cd5a35db06..6aeaadd81a4 100644 --- a/apps/browser/src/_locales/ar/messages.json +++ b/apps/browser/src/_locales/ar/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "تطبيق المصادقة" }, - "authenticatorAppDesc": { - "message": "استخدام تطبيق مصادقة (مثل Authy أو Google Authenticator) لإنشاء رموز تحقق مستندة إلى الوقت.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "مفتاح أمان YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "استخدم YubiKey للوصول إلى حسابك. يعمل مع YubiKey 4 ،4 Nano ،4C، وأجهزة NEO." }, - "duoDesc": { - "message": "التحقق باستخدام نظام الحماية الثنائي باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "البريد الإلكتروني" }, - "emailDesc": { - "message": "سيتم إرسال رمز التحقق إليك بالبريد الإلكتروني." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "البيئة المستضافة ذاتيا" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "تأكيد كلمة مرور الملف" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json index 4a01675331d..c9d68c88ebb 100644 --- a/apps/browser/src/_locales/az/messages.json +++ b/apps/browser/src/_locales/az/messages.json @@ -123,7 +123,7 @@ "message": "Parol yarat (kopyalandı)" }, "copyElementIdentifier": { - "message": "Özəl sahə adını kopyala" + "message": "Özəl xana adını kopyala" }, "noMatchingLogins": { "message": "Uyuşan giriş məlumatları yoxdur" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Kimlik doğrulayıcı tətbiqi" }, - "authenticatorAppDesc": { - "message": "Vaxt əsaslı doğrulama kodları yaratmaq üçün (Authy və ya Google Authenticator kimi) kimlik doğrulayıcı tətbiq istifadə edin.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Bitwarden Authenticator kimi bir kimlik doğrulama tətbiqi tərəfindən yaradılan kodu daxil edin.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP güvənlik açarı" + "yubiKeyTitleV2": { + "message": "Yubico OTP Güvənlik Açarı" }, "yubiKeyDesc": { "message": "Hesabınıza müraciət etmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." }, - "duoDesc": { - "message": "Duo Security ilə doğrulamaq üçün Duo Mobile tətbiqi, SMS, telefon zəngi və ya U2F güvənlik açarını istifadə edin.", + "duoDescV2": { + "message": "Duo Security tərəfindən yaradılan kodu daxil edin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,17 +1132,17 @@ "emailTitle": { "message": "E-poçt" }, - "emailDesc": { - "message": "Doğrulama kodları e-poçt ünvanınıza göndəriləcək." + "emailDescV2": { + "message": "E-poçtunuza göndərilən kodu daxil edin." }, "selfHostedEnvironment": { - "message": "Öz-özünə sahiblik edən mühit" + "message": "Self-hosted mühit" }, "selfHostedEnvironmentFooter": { - "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının baza URL-sini müəyyənləşdirin." + "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının təməl URL-sini müəyyənləşdirin." }, "selfHostedBaseUrlHint": { - "message": "Şirkət daxili sahiblik edən Bitwarden quraşdırmasının təməl URL-sini qeyd edin. Nümunə: https://bitwarden.company.com" + "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının təməl URL-sini müəyyənləşdirin. Nümunə: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "Qabaqcıl konfiqurasiya üçün hər xidmətin təməl URL-sini müstəqil olaraq qeyd edə bilərsiniz." @@ -1178,7 +1178,7 @@ "message": "Mühit URL-ləri saxlanıldı." }, "showAutoFillMenuOnFormFields": { - "message": "Form sahələrində avto-doldurma menyusunu göstər", + "message": "Form xanalarında avto-doldurma menyusunu göstər", "description": "Represents the message for allowing the user to enable the auto-fill overlay" }, "showAutoFillMenuOnFormFieldsDescAlt": { @@ -1195,7 +1195,7 @@ "description": "Overlay setting select option for disabling autofill overlay" }, "autofillOverlayVisibilityOnFieldFocus": { - "message": "Sahə seçiləndə (fokusda)", + "message": "Xana seçildikdə (fokusda)", "description": "Overlay appearance select option for showing the field on focus of the input element" }, "autofillOverlayVisibilityOnButtonClick": { @@ -1248,7 +1248,7 @@ "message": "Anbarı kilidlə" }, "customFields": { - "message": "Özəl sahələr" + "message": "Özəl xanalar" }, "copyValue": { "message": "Dəyəri kopyala" @@ -1257,7 +1257,7 @@ "message": "Dəyər" }, "newCustomField": { - "message": "Yeni özəl sahə" + "message": "Yeni özəl xana" }, "dragToSort": { "message": "Sıralamaq üçün sürüklə" @@ -2265,7 +2265,7 @@ "message": "Unikal identifikator tapılmadı." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$, öz-özünə sahiblik edən açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", + "message": "$ORGANIZATION$, self-hosted açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", "placeholders": { "organization": { "content": "$1", @@ -2513,7 +2513,7 @@ "message": "Server versiyası" }, "selfHostedServer": { - "message": "öz-özünə sahiblik edən" + "message": "self-hosted" }, "thirdParty": { "message": "Üçüncü tərəf" @@ -2803,7 +2803,7 @@ "message": "Giriş, bir e-poçt ünvanı deyil." }, "fieldsNeedAttention": { - "message": "Yuxarıdakı $COUNT$ sahənin diqqətinizə ehtiyacı var.", + "message": "Yuxarıdakı $COUNT$ xananın diqqətinizə ehtiyacı var.", "placeholders": { "count": { "content": "$1", @@ -2886,7 +2886,7 @@ "description": "Toast message for informing the user that auto-fill on page load has been set to the default setting." }, "turnOffMasterPasswordPromptToEditField": { - "message": "Bu sahəyə düzəliş etmək üçün \"Ana parolu təkrar soruş\"u söndürün", + "message": "Bu xanaya düzəliş etmək üçün \"Ana parolu təkrar soruş\"u söndürün", "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Fayl parolunu təsdiqlə" }, + "exportSuccess": { + "message": "Anbar datası xaricə köçürüldü" + }, "typePasskey": { "message": "Keçid açarı" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Qovluğu olmayan elementlər" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Təşkilat deaktiv edildi" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Deaktiv edilmiş təşkilatlardakı elementlərə müraciət edilə bilməz. Kömək üçün təşkilatınızın sahibi ilə əlaqə saxlayın." }, + "upload": { + "message": "Yüklə" + }, + "addAttachment": { + "message": "Qoşma əlavə et" + }, + "maxFileSizeSansPunctuation": { + "message": "Maksimal fayl həcmi 500 MB-dır" + }, + "deleteAttachmentName": { + "message": "$NAME$ qoşmasını sil", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "$NAME$ endir", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Bu qoşmanı birdəfəlik silmək istədiyinizə əminsiniz?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Ödənişsiz təşkilatlar qoşmaları istifadə edə bilməz" + }, "filters": { - "message": "Filters" + "message": "Filtrlər" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json index 404324e3c15..d855d8447f2 100644 --- a/apps/browser/src/_locales/be/messages.json +++ b/apps/browser/src/_locales/be/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Праграма аўтэнтыфікацыі" }, - "authenticatorAppDesc": { - "message": "Выкарыстоўвайце праграму праграму аўтэнтыфікацыі (напрыклад, Authy або Google Authenticator) для генерацыі праверачных кодаў на падставе часу.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ бяспекі YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Выкарыстоўвайце YubiKey для доступу да вашага ўліковага запісу. Працуе з ключамі бяспекі YubiKey 4, 4 Nano, 4C і NEO." }, - "duoDesc": { - "message": "Праверка з дапамогай Duo Security, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Электронная пошта" }, - "emailDesc": { - "message": "Праверачныя коды будуць адпраўляцца вам па электронную пошту." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Асяроддзе ўласнага хостынгу" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Пацвердзіць пароль файла" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json index 4d769bfa0c6..360d73d41d8 100644 --- a/apps/browser/src/_locales/bg/messages.json +++ b/apps/browser/src/_locales/bg/messages.json @@ -102,7 +102,7 @@ "message": "Копиране на потребителското име" }, "copyNumber": { - "message": "Копиране на но̀мера" + "message": "Копиране на номера" }, "copySecurityCode": { "message": "Копиране на кода за сигурност" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Приложение за удостоверяване" }, - "authenticatorAppDesc": { - "message": "Използвайте приложение за удостоверяване (като Authy или Google Authenticator) за генерирането на временни кодове за потвърждение.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Въведете код създаден чрез приложение за удостоверяване, като например това на Битуорден.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "Ключ за сигурност YubiKey OTP" }, "yubiKeyDesc": { "message": "Използвайте ключа за сигурност YubiKey, за да влезете в акаунта си. Работи с устройствата YubiKey 4, 4 Nano, 4C и NEO." }, - "duoDesc": { - "message": "Удостоверяване чрез Duo Security, с ползване на приложението Duo Mobile, SMS, телефонен разговор или устройство U2F.", + "duoDescV2": { + "message": "Въведете код създаден чрез Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Електронна поща" }, - "emailDesc": { - "message": "Кодовете за потвърждение ще ви бъдат пратени по е-поща." + "emailDescV2": { + "message": "Въведете кода изпратен на е-пощата Ви." }, "selfHostedEnvironment": { "message": "Собствена среда" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Потвърждаване на паролата на файла" }, + "exportSuccess": { + "message": "Данните от трезора са изнесени" + }, "typePasskey": { "message": "Секретен ключ" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Елементи без папка" }, + "itemDetails": { + "message": "Подробности за елемент" + }, + "itemName": { + "message": "Име на елемент" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Не можете да премахнете колекции с права „Само за преглед“: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Организацията е деактивирана" }, + "owner": { + "message": "Собственик" + }, + "selfOwnershipLabel": { + "message": "Вие", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Записите в деактивирани организации не са достъпни. Свържете се със собственика на организацията си за помощ." }, + "upload": { + "message": "Качване" + }, + "addAttachment": { + "message": "Добавяне на прикачен файл" + }, + "maxFileSizeSansPunctuation": { + "message": "Максималният размер на файла е 500 MB" + }, + "deleteAttachmentName": { + "message": "Изтриване на прикачения файл $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Сваляне на $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Наистина ли искате да изтриете завинаги този прикачен файл?" + }, + "premium": { + "message": "Премиум" + }, + "freeOrgsCannotUseAttachments": { + "message": "Безплатните организации не могат да използват прикачени файлове" + }, "filters": { - "message": "Filters" + "message": "Филтри" + }, + "cardDetails": { + "message": "Данни за картата" + }, + "cardBrandDetails": { + "message": "$BRAND$ подробности", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json index 8c5db7d4936..cffb78f5b46 100644 --- a/apps/browser/src/_locales/bn/messages.json +++ b/apps/browser/src/_locales/bn/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "প্রমাণীকরণকারী অ্যাপ" }, - "authenticatorAppDesc": { - "message": "সময় ভিত্তিক যাচাইকরণ কোড উৎপন্ন করতে একটি প্রমাণীকরণকারী অ্যাপ্লিকেশন (যেমন Authy বা Google Authenticator) ব্যবহার করুন।", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP সুরক্ষা কী" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "আপনার অ্যাকাউন্ট ব্যাবহার করতে একটি YubiKey ব্যবহার করুন। YubiKey 4, 4 Nano, 4C, এবং NEO ডিভাইসগুলির সাথে কাজ করে।" }, - "duoDesc": { - "message": "Duo Mobile app, এসএমএস, ফোন কল, বা U2F সুরক্ষা কী ব্যবহার করে Duo Security এর মাধ্যমে যাচাই করুন।", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ই-মেইল" }, - "emailDesc": { - "message": "যাচাই কোডগুলি আপনাকে ই-মেইল করা হবে।" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "স্ব-হোস্টকৃত পরিবেশ" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json index f49a9148aec..48159dcd6d2 100644 --- a/apps/browser/src/_locales/bs/messages.json +++ b/apps/browser/src/_locales/bs/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json index 00b4088eb7a..3d7ae128fbc 100644 --- a/apps/browser/src/_locales/ca/messages.json +++ b/apps/browser/src/_locales/ca/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicació autenticadora" }, - "authenticatorAppDesc": { - "message": "Utilitzeu una aplicació autenticadora (com Authy o Google Authenticator) per generar codis de verificació basats en el temps.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Clau de seguretat OTP de YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." }, - "duoDesc": { - "message": "Verifiqueu amb Duo Security mitjançant l'aplicació Duo Mobile, SMS, trucada telefònica o clau de seguretat U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Correu electrònic" }, - "emailDesc": { - "message": "Els codis de verificació els rebreu per correu electrònic." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Entorn d'allotjament propi" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirma la contrasenya del fitxer" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Clau de pas" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json index 8f0d3901d65..74c8ce12125 100644 --- a/apps/browser/src/_locales/cs/messages.json +++ b/apps/browser/src/_locales/cs/messages.json @@ -409,7 +409,7 @@ "message": "Oblíbené" }, "unfavorite": { - "message": "Unfavorite" + "message": "Odebrat z oblíbených" }, "itemAddedToFavorites": { "message": "Položka byla přidána do oblíbených" @@ -439,7 +439,7 @@ "message": "Spustit" }, "launchWebsite": { - "message": "Launch website" + "message": "Otevřít webovou stránku" }, "website": { "message": "Webová stránka" @@ -591,7 +591,7 @@ "message": "Byli jste úspěšně přihlášeni" }, "youMayCloseThisWindow": { - "message": "Nyní můžete toto okno zavřít" + "message": "Toto okno můžete zavřít" }, "masterPassSent": { "message": "Poslali jsme Vám e-mail s nápovědou k hlavnímu heslu." @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Ověřovací aplikace" }, - "authenticatorAppDesc": { - "message": "Použijte ověřovací aplikaci (jako je Authy nebo Google Authenticator) pro generování časově omezených kódů.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "Bezpečnostní klíč YubiKey OTP" }, "yubiKeyDesc": { "message": "Použije YubiKey pro přístup k Vašemu trezoru. Podporuje YubiKey 4, 4 Nano, 4C a NEO." }, - "duoDesc": { - "message": "Ověření pomocí Duo Security prostřednictvím aplikace Duo Mobile, SMS, telefonního hovoru nebo bezpečnostního klíče U2F.", + "duoDescV2": { + "message": "Zadejte kód vygenerovaný DUO Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Ověřovací kódy Vám budou zaslány e-mailem." + "emailDescV2": { + "message": "Zadejte kód odeslaný na Váš e-mail." }, "selfHostedEnvironment": { "message": "Vlastní hostované prostředí" @@ -1481,7 +1481,7 @@ "message": "Kolekce" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ kolekcí", "placeholders": { "count": { "content": "$1", @@ -1719,7 +1719,7 @@ "message": "Automaticky vyplnit a uložit" }, "fillAndSave": { - "message": "Fill and save" + "message": "Vyplnit a uložit" }, "autoFillSuccessAndSavedUri": { "message": "Položka byla automaticky vyplněna a URI bylo uloženo" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Potvrzení hesla souboru" }, + "exportSuccess": { + "message": "Data trezoru byla exportována" + }, "typePasskey": { "message": "Přístupový klíč" }, @@ -3424,7 +3427,7 @@ "message": "Žádné hodnoty ke zkopírování" }, "assignCollections": { - "message": "Assign collections" + "message": "Přiřadit kolekce" }, "copyEmail": { "message": "Kopírovat e-mail" @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Položky bez složky" }, + "itemDetails": { + "message": "Detaily položky" + }, + "itemName": { + "message": "Název položky" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organizace je deaktivována" }, + "owner": { + "message": "Vlastník" + }, + "selfOwnershipLabel": { + "message": "Vy", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "K položkám v deaktivované organizaci nemáte přístup. Požádejte o pomoc vlastníka organizace." }, + "upload": { + "message": "Nahrát" + }, + "addAttachment": { + "message": "Přidat přílohu" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximální velikost souboru je 500 MB" + }, + "deleteAttachmentName": { + "message": "Smazat přílohu $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Stáhnout $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Opravdu chcete tuto přílohu navždy smazat?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Volné organizace nemohou používat přílohy" + }, "filters": { "message": "Filtry" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json index 38a276406ac..ae3bb72c0b0 100644 --- a/apps/browser/src/_locales/cy/messages.json +++ b/apps/browser/src/_locales/cy/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Ap dilysu" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Ebost" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json index cb525a2373c..a8aeac4ef6e 100644 --- a/apps/browser/src/_locales/da/messages.json +++ b/apps/browser/src/_locales/da/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentificerings-app" }, - "authenticatorAppDesc": { - "message": "Brug en autentificerings app (f.eks. Authy eller Google Autentificering) til at generere tidsbaserede bekræftelseskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Angiv en kode genereret af en godkendelses-app såsom Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP sikkerhedsnøgle" + "yubiKeyTitleV2": { + "message": "Yubico OTP-sikkerhedsnøgle" }, "yubiKeyDesc": { "message": "Brug en YubiKey til at få adgang til din konto. Virker med YubiKey 4, 4 Nano, 4C og NEO enheder." }, - "duoDesc": { - "message": "Bekræft med Duo sikkerhed ved hjælp af Duo Mobile app, SMS, telefonopkald eller U2F sikkerhedsnøgle.", + "duoDescV2": { + "message": "Angiv en kode genereret af Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Bekræftelseskoder vil blive e-mailet til dig." + "emailDescV2": { + "message": "Angiv en kode tilsendt pr. e-mail." }, "selfHostedEnvironment": { "message": "Selv-hosted miljø" @@ -1142,13 +1142,13 @@ "message": "Angiv grund-URL'en i din lokal-hostede Bitwarden-installation." }, "selfHostedBaseUrlHint": { - "message": "Specify the base URL of your on-premises hosted Bitwarden installation. Example: https://bitwarden.company.com" + "message": "Angiv basis-URL'en for den lokalt-hosted Bitwarden-installation. Eks.: https://bitwarden.firma.dk" }, "selfHostedCustomEnvHeader": { - "message": "For advanced configuration, you can specify the base URL of each service independently." + "message": "Til avancerede brugere. Man kan angive basis-URL'en for hver tjeneste uafhængigt." }, "selfHostedEnvFormInvalid": { - "message": "You must add either the base Server URL or at least one custom environment." + "message": "Der skal tilføjes enten basis server-URL'en eller mindst ét tilpasset miljø." }, "customEnvironment": { "message": "Brugerdefineret miljø" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Bekræft filadgangskode" }, + "exportSuccess": { + "message": "Boksdata eksporteret" + }, "typePasskey": { "message": "Adgangsnøgle" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Emner uden mappe" }, + "itemDetails": { + "message": "Emnedetaljer" + }, + "itemName": { + "message": "Emnenavn" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisation er deaktiveret" }, + "owner": { + "message": "Ejer" + }, + "selfOwnershipLabel": { + "message": "Dig", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Emner i deaktiverede organisationer kan ikke tilgås. Kontakt organisationsejeren for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Tilføj vedhæftning" + }, + "maxFileSizeSansPunctuation": { + "message": "Maks. filstørrelse er 500 MB" + }, + "deleteAttachmentName": { + "message": "Slet vedhæftelse $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Sikker på, at denne vedhæftning skal slettes permanent?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Gratis organisationer kan ikke bruge vedhæftninger" + }, "filters": { - "message": "Filters" + "message": "Filtre" + }, + "cardDetails": { + "message": "Kortoplysninger" + }, + "cardBrandDetails": { + "message": "$BRAND$ oplysninger", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json index ef1ebaf21eb..11900e883bd 100644 --- a/apps/browser/src/_locales/de/messages.json +++ b/apps/browser/src/_locales/de/messages.json @@ -17,7 +17,7 @@ "message": "Konto erstellen" }, "setAStrongPassword": { - "message": "Ein starkes Passwort festlegen" + "message": "Lege ein starkes Passwort fest" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Schließe die Erstellung deines Kontos ab, indem du ein Passwort festlegst" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator App" }, - "authenticatorAppDesc": { - "message": "Verwende eine Authentifizierungs-App (wie zum Beispiel Authy oder Google Authenticator), um zeitbasierte Verifizierungscodes zu generieren.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Sicherheitsschlüssel" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey um auf dein Konto zuzugreifen. Funtioniert mit YubiKey 4, Nano 4, 4C und NEO Geräten." }, - "duoDesc": { - "message": "Verifiziere mit Duo Security, indem du die Duo Mobile App, SMS, Anrufe oder U2F Sicherheitsschlüssel benutzt.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-Mail" }, - "emailDesc": { - "message": "Bestätigungscodes werden Ihnen per E-Mail zugesandt." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Selbst gehostete Umgebung" @@ -1806,13 +1806,13 @@ "message": "Deabonnieren" }, "atAnyTime": { - "message": "at any time." + "message": "jederzeit." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "Indem Sie fortfahren, stimmen Sie unseren" }, "and": { - "message": "and" + "message": "und" }, "acceptPolicies": { "message": "Durch Anwählen dieses Kästchens erklärst du dich mit Folgendem einverstanden:" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Dateipasswort bestätigen" }, + "exportSuccess": { + "message": "Tresor-Daten exportiert" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Einträge ohne Ordner" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisation ist deaktiviert" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Auf Einträge in deaktivierten Organisationen kann nicht zugegriffen werden. Kontaktiere deinen Organisationseigentümer für Unterstützung." }, + "upload": { + "message": "Hochladen" + }, + "addAttachment": { + "message": "Anhang hinzufügen" + }, + "maxFileSizeSansPunctuation": { + "message": "Die maximale Dateigröße beträgt 500 MB" + }, + "deleteAttachmentName": { + "message": "Datei $NAME$ löschen", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "$NAME$ herunterladen", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Sind Sie sich sicher, dass Sie diesen Anhang dauerhaft löschen möchten?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filter" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json index 8de4246422a..765738bcade 100644 --- a/apps/browser/src/_locales/el/messages.json +++ b/apps/browser/src/_locales/el/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Εφαρμογή ελέγχου ταυτότητας" }, - "authenticatorAppDesc": { - "message": "Χρησιμοποιήστε μια εφαρμογή επαλήθευσης (όπως το Authy ή Google Authenticator) για να δημιουργήσει κωδικούς επαλήθευσης με χρόνικο περιορισμό.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Κλειδί ασφαλείας YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Χρησιμοποιήστε ένα YubiKey για να αποκτήσετε πρόσβαση στο λογαριασμό σας. Λειτουργεί με συσκευές σειράς YubiKey 4, 4 Nano, 4C και συσκευές NEO." }, - "duoDesc": { - "message": "Επαληθεύστε με το Duo Security χρησιμοποιώντας την εφαρμογή Duo Mobile, μηνύματα SMS, τηλεφωνική κλήση ή κλειδί ασφαλείας U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Οι κωδικοί επαλήθευσης θα σας αποσταλούν μέσω ηλεκτρονικού ταχυδρομείου." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Αυτο-φιλοξενούμενο περιβάλλον" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 64f039bb8b2..22e0eba1b4f 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -556,6 +556,18 @@ "security": { "message": "Security" }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "errorOccurred": { "message": "An error has occurred" }, @@ -1105,18 +1117,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1144,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -1471,6 +1483,15 @@ } } }, + "viewItemHeader": { + "message": "View $TYPE$", + "placeholders": { + "type": { + "content": "$1", + "example": "Login" + } + } + }, "passwordHistory": { "message": "Password history" }, @@ -2164,6 +2185,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature. You can verify your email in the web vault." }, @@ -3492,13 +3516,125 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "additionalInformation": { + "message": "Additional information" + }, + "itemHistory": { + "message": "Item history" + }, + "lastEdited": { + "message": "Last edited" + }, + "ownerYou":{ + "message": "Owner: You" + }, + "linked": { + "message": "Linked" + }, + "copySuccessful": { + "message": "Copy Successful" + }, + "upload": { + "message": "Upload" + }, + "addAttachment":{ + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "personalDetails": { + "message": "Personal details" + }, + "identification": { + "message": "Identification" + }, + "contactInfo": { + "message": "Contact info" + }, + "downloadAttachment": { + "message": "Download - $ITEMNAME$", + "placeholders": { + "itemname": { + "content": "$1", + "example": "Your File" + } + } + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json index 4cf3cfcfcb6..9160b95ed22 100644 --- a/apps/browser/src/_locales/en_GB/messages.json +++ b/apps/browser/src/_locales/en_GB/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisation is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organisations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json index 40613be7088..d2793d4bd4e 100644 --- a/apps/browser/src/_locales/en_IN/messages.json +++ b/apps/browser/src/_locales/en_IN/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisation is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organisations cannot be accessed. Contact your organisation owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organisations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json index 29351836665..85cf8230698 100644 --- a/apps/browser/src/_locales/es/messages.json +++ b/apps/browser/src/_locales/es/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicación de autenticación" }, - "authenticatorAppDesc": { - "message": "Utiliza una aplicación de autenticación (como Authy o Google Authenticator) para generar código de verificación basados en tiempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Introduce un código generado por una aplicación de autenticación como Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Llave de seguridad YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Llave de seguridad Yubico OTP" }, "yubiKeyDesc": { "message": "Usa un Yubikey para acceder a tu cuenta. Funciona con YubiKey 4, 4 Nano, 4C y dispositivos NEO." }, - "duoDesc": { - "message": "Verificar con Duo Security usando la aplicación Duo Mobile, SMS, llamada telefónica o llave de seguridad U2F.", + "duoDescV2": { + "message": "Introduce un código generado por Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Correo electrónico" }, - "emailDesc": { - "message": "Los códigos de verificación te serán enviados por correo electrónico." + "emailDescV2": { + "message": "Introduce un código enviado a tu correo electrónico." }, "selfHostedEnvironment": { "message": "Entorno de alojamiento propio" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirmar contraseña del archivo" }, + "exportSuccess": { + "message": "Datos de la caja fuerte exportados" + }, "typePasskey": { "message": "Clave de acceso" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Elementos sin carpeta" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "La organización está desactivada" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "No se puede acceder a los elementos de las organizaciones desactivadas. Ponte en contacto con el propietario de tu organización para obtener ayuda." }, + "upload": { + "message": "Subir" + }, + "addAttachment": { + "message": "Añadir adjunto" + }, + "maxFileSizeSansPunctuation": { + "message": "El tamaño máximo del fichero es de 500 MB" + }, + "deleteAttachmentName": { + "message": "Elimina el adjunto $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Descargar $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "¿Estás seguro de que deseas eliminar permanentemente este adjunto?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Las organizaciones gratis no pueden usar archivos adjuntos" + }, "filters": { - "message": "Filters" + "message": "Filtros" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json index 498d90a5fdd..08f70aff2af 100644 --- a/apps/browser/src/_locales/et/messages.json +++ b/apps/browser/src/_locales/et/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentimise rakendus" }, - "authenticatorAppDesc": { - "message": "Kausta autentimise rakendust (näiteks Authy või Google Authenticator), et luua ajal baseeruvaid kinnituskoode.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Turvaline võti" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Kasuta kontole ligipääsemiseks YubiKey-d. See töötab YubiKey 4, 4 Nano, 4C ja NEO seadmetega." }, - "duoDesc": { - "message": "Kinnita Duo Security abil, kasutades selleks Duo Mobile rakendust, SMS-i, telefonikõnet või U2F turvavõtit.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Kinnituskoodid saadetakse e-postiga." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted Environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json index 0dd330314a0..1c98122849d 100644 --- a/apps/browser/src/_locales/eu/messages.json +++ b/apps/browser/src/_locales/eu/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentifikazio aplikazioa" }, - "authenticatorAppDesc": { - "message": "Erabili autentifikazio aplikazio bat (adibidez, Authy edo Google Authenticator) denboran oinarritutako egiaztatze-kodeak sortzeko.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP segurtasun-gakoa" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Erabili YubiKey zure kontuan sartzeko. YubiKey 4, 4 Nano, 4C eta NEO gailuekin dabil." }, - "duoDesc": { - "message": "Egiaztatu Duo Securityrekin Duo Mobile aplikazioa, SMS, telefono deia edo U2F segurtasun-gakoa erabiliz.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Emaila" }, - "emailDesc": { - "message": "Egiaztatze-kodeak email bidez bidaliko dira." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Ostatze ingurune propioa" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json index 44d9e193df3..003b8667fa5 100644 --- a/apps/browser/src/_locales/fa/messages.json +++ b/apps/browser/src/_locales/fa/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "برنامه احراز هویت" }, - "authenticatorAppDesc": { - "message": "از یک برنامه احراز هویت (مانند Authy یا Google Authenticator) استفاده کنید تا کدهای تأیید بر پایه زمان تولید کنید.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "کلید امنیتی YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "از یک YubiKey برای دسترسی به حسابتان استفاده کنید. همراه با دستگاه‌های YubiKey 4 ،4 Nano ،NEO کار می‌کند." }, - "duoDesc": { - "message": "با Duo Security با استفاده از برنامه تلفن همراه، پیامک، تماس تلفنی، یا کلید امنیتی U2F تأیید کنید.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ایمیل" }, - "emailDesc": { - "message": "کد تأیید برایتان ارسال می‌شود." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "محیط خود میزبان" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json index 5892908f5e4..a8e978b0beb 100644 --- a/apps/browser/src/_locales/fi/messages.json +++ b/apps/browser/src/_locales/fi/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Todennussovellus" }, - "authenticatorAppDesc": { - "message": "Käytä todennussovellusta (kuten Authy, Google tai Microsoft Authenticator) luodaksesi aikarajallisia todennuskoodeja.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Syötä Bitwarden Authenticatorin kaltaisen todennusovelluksen luoma koodi.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP -todennuslaite" + "yubiKeyTitleV2": { + "message": "Yubico OTP -todennuslaite" }, "yubiKeyDesc": { "message": "Käytä YubiKey-todennuslaitetta tilisi avaukseen. Toimii YubiKey 4, 4 Nano, 4C sekä NEO -laitteiden kanssa." }, - "duoDesc": { - "message": "Vahvista Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-todennuslaitetta.", + "duoDescV2": { + "message": "Syötä Duo Securityn luoma koodi.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Sähköposti" }, - "emailDesc": { - "message": "Todennuskoodit lähetetään sinulle sähköpostitse." + "emailDescV2": { + "message": "Syötä sähköpostiisi lähetetty koodi." }, "selfHostedEnvironment": { "message": "Itse ylläpidetty palvelinympäristö" @@ -2890,7 +2890,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Näytä/piilota sivuvalikko" }, "skipToContent": { "message": "Siirry sisältöön" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Vahvista tiedoston salasana" }, + "exportSuccess": { + "message": "Holvin tiedot on viety" + }, "typePasskey": { "message": "Suojausavain" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Kansiottomat kohteet" }, + "itemDetails": { + "message": "Kohteen tiedot" + }, + "itemName": { + "message": "Kohteen nimi" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Et voi poistaa kokoelmia Vain katselu -oikeuksilla: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisaatio on poistettu käytöstä" }, + "owner": { + "message": "Omistaja" + }, + "selfOwnershipLabel": { + "message": "Sinä", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Käytöstä poistettujen organisaatioiden kohteet eivät ole käytettävissä. Ole yhteydessä organisaation omistajaan saadaksesi apua." }, + "upload": { + "message": "Lähetä" + }, + "addAttachment": { + "message": "Lisää liite" + }, + "maxFileSizeSansPunctuation": { + "message": "Tiedoston enimmäiskoko on 500 Mt" + }, + "deleteAttachmentName": { + "message": "Poista liite $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Lataa $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Haluatko varmasti poistaa tämän liitteen pysyvästi?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Ilmaiset organisaatiot eivät voi käyttää liitteitä" + }, "filters": { - "message": "Filters" + "message": "Suodattimet" + }, + "cardDetails": { + "message": "Kortin tiedot" + }, + "cardBrandDetails": { + "message": "$BRAND$-tiedot", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json index d477f3f75d2..54e7ef33b93 100644 --- a/apps/browser/src/_locales/fil/messages.json +++ b/apps/browser/src/_locales/fil/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "App ng Authenticator" }, - "authenticatorAppDesc": { - "message": "Gamitin ang isang authenticator app (tulad ng Authy o Google Authenticator) upang lumikha ng time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Gamitin ang YubiKey upang ma-access ang iyong account. Gumagana sa mga YubiKey 4, 4 Nano, 4C, at NEO devices." }, - "duoDesc": { - "message": "Patunayan sa pamamagitan ng Duo Security gamit ang Duo Mobile app, SMS, tawag sa telepono, o key ng seguridad ng U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Mag-email" }, - "emailDesc": { - "message": "Mga kodigong pang-pagpapatunay ang ipapadala sa iyo sa pamamagitan ng email." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Kapaligirang self-hosted" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json index 9c18144f2ef..1278470313a 100644 --- a/apps/browser/src/_locales/fr/messages.json +++ b/apps/browser/src/_locales/fr/messages.json @@ -17,10 +17,10 @@ "message": "Créer un compte" }, "setAStrongPassword": { - "message": "Set a strong password" + "message": "Définir un mot de passe fort" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "Finish creating your account by setting a password" + "message": "Terminer la création de votre compte en définissant un mot de passe" }, "login": { "message": "Se connecter" @@ -50,7 +50,7 @@ "message": "Un indice de mot de passe principal peut vous aider à vous souvenir de votre mot de passe si vous l'oubliez." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Si vous oubliez votre mot de passe, l'indice peut être envoyé à votre courriel. $CURRENT$/$MAXIMUM$ caractères maximum.", "placeholders": { "current": { "content": "$1", @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Application d'authentification" }, - "authenticatorAppDesc": { - "message": "Utiliser une application d'authentification (comme Authy ou Google Authenticator) pour générer des codes de vérification basés sur le temps.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Clé de sécurité YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, - "duoDesc": { - "message": "S'authentifier avec Duo Security via l'application Duo Mobile, un SMS, un appel téléphonique, ou une clé de sécurité U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Courriel" }, - "emailDesc": { - "message": "Les codes de vérification vous seront envoyés par courriel." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Environnement auto-hébergé" @@ -1454,7 +1454,7 @@ "message": "Identité" }, "newItemHeader": { - "message": "New $TYPE$", + "message": "Nouveau/nouvelle $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1463,7 +1463,7 @@ } }, "editItemHeader": { - "message": "Edit $TYPE$", + "message": "Éditer $TYPE$", "placeholders": { "type": { "content": "$1", @@ -1481,7 +1481,7 @@ "message": "Collections" }, "nCollections": { - "message": "$COUNT$ collections", + "message": "$COUNT$ collection(s)", "placeholders": { "count": { "content": "$1", @@ -1803,16 +1803,16 @@ "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Se désabonner" }, "atAnyTime": { - "message": "at any time." + "message": "à tout moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuant, vous acceptez les" }, "and": { - "message": "and" + "message": "et" }, "acceptPolicies": { "message": "En cochant cette case vous acceptez ce qui suit :" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirmez le mot de passe du fichier" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Clé d'identification (passkey)" }, @@ -3421,7 +3424,7 @@ } }, "noValuesToCopy": { - "message": "No values to copy" + "message": "Aucune valeur à copier" }, "assignCollections": { "message": "Assigner une collection" @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Eléments sans dossier" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "L'organisation est désactivée" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Les éléments des Organisations désactivées ne sont pas accessibles. Contactez le propriétaire de votre Organisation pour obtenir de l'aide." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { - "message": "Filters" + "message": "Filtres" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json index 2e3c478cb54..85239caddb5 100644 --- a/apps/browser/src/_locales/gl/messages.json +++ b/apps/browser/src/_locales/gl/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicación de autenticación" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Correo electrónico" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Entorno de auto-aloxamento" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json index e9bdad57d6e..ca67598a2e1 100644 --- a/apps/browser/src/_locales/he/messages.json +++ b/apps/browser/src/_locales/he/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "אפליקציית אימות" }, - "authenticatorAppDesc": { - "message": "השתמש באפליקצית אימות (כמו לדוגמא Authy או Google Authenticator) לייצור סיסמאות אימות מבוססות זמן.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "מפתח אבטחה OTP של YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "השתמש בYubiKey עבור גישה לחשבון שלך. עובד עם YubiKey בגירסאות 4, 4C, 4Nano, ומכשירי NEO." }, - "duoDesc": { - "message": "בצע אימות מול Duo Security באמצעות אפליקצית Duo לפלאפון, SMS, שיחת טלפון, או מפתח אבטחה U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "אימייל" }, - "emailDesc": { - "message": "קודי אימות ישלחו לאימייל שלך." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "סביבה על שרתים מקומיים" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json index 3442987a3d3..5004beb376b 100644 --- a/apps/browser/src/_locales/hi/messages.json +++ b/apps/browser/src/_locales/hi/messages.json @@ -3,7 +3,7 @@ "message": "bitwarden" }, "extName": { - "message": "Bitwarden Password Manager", + "message": "बिटवार्डन पासवर्ड मैनेजर", "description": "Extension name, MUST be less than 40 characters (Safari restriction)" }, "extDesc": { @@ -224,7 +224,7 @@ "message": "Log Out" }, "aboutBitwarden": { - "message": "About Bitwarden" + "message": "बिटवार्डन का परिचय" }, "about": { "message": "जानकारी" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator App" }, - "authenticatorAppDesc": { - "message": "समय-आधारित सत्यापन कोड उत्पन्न करने के लिए एक प्रमाणक ऐप (जैसे Authy या Google Authenticator) का उपयोग करें।", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "अपने खाते तक पहुंचने के लिए YubiKey का उपयोग करें। YubiKey 4, 4 नैनो, 4C, और NEO उपकरणों के साथ काम करता है।" }, - "duoDesc": { - "message": "डुओ मोबाइल ऐप, एसएमएस, फोन कॉल या U2F सुरक्षा कुंजी का उपयोग करके डुओ सिक्योरिटी के साथ सत्यापित करें।", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ईमेल" }, - "emailDesc": { - "message": "सत्यापन कोड आपको ईमेल किए जाएंगे।" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted Environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "वॉल्ट डेटा निर्यात किया गया" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { - "message": "Filters" + "message": "फ़िल्टर" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json index 3067103d7be..77bc1d37612 100644 --- a/apps/browser/src/_locales/hr/messages.json +++ b/apps/browser/src/_locales/hr/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentifikatorska aplikacija" }, - "authenticatorAppDesc": { - "message": "Koristi autentifikatorsku aplikaciju (npr. Authy ili Google Authentifikator) za generiranje kontrolnih kodova.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP sigurnosni ključ" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Koristi YubiKey za pristup svojem računu. Radi s YubiKey 4, 4 Nano, 4C i NEO uređajima." }, - "duoDesc": { - "message": "Potvrdi s Duo Security pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-pošta" }, - "emailDesc": { - "message": "Verifikacijski kodovi će biti poslani e-poštom." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Vlastito hosting okruženje" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Potvrdi lozinku datoteke" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Pristupni ključ" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json index 3d457670136..d4106a0e555 100644 --- a/apps/browser/src/_locales/hu/messages.json +++ b/apps/browser/src/_locales/hu/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Hitelesítő alkalmazás" }, - "authenticatorAppDesc": { - "message": "Használj egy másik alkalmazást (mint például az Authy vagy a Google Authenticator) idő alapú ellenőrzőkód generálásához.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Adjunk meg egy hitelesítő alkalmazás, például a Bitwarden Authenticator által generált kódot.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "YubiKey OTP biztonsági kulcs" }, "yubiKeyDesc": { "message": "Használj egy YubiKey-t, hogy hozzá férhess a felhasználódhoz. Működik a YubiKey 4, 4 Nano, 4C, és NEO eszközökkel." }, - "duoDesc": { - "message": "Ellenőrizd Duo Security-val a Duo Mobile alkalmazás, SMS, telefon hívás vagy U2F biztonsági kulcs segítségével.", + "duoDescV2": { + "message": "Adjuk meg a Duo Security által generált kódot.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Ellenőrző kódok el lesznek e-mailbe küldve neked." + "emailDescV2": { + "message": "Adjuk meg az email címre elküldött kódot." }, "selfHostedEnvironment": { "message": "Saját üzemeltetésű környezet" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Fájl jelszó megerősítés" }, + "exportSuccess": { + "message": "A széfadatok exportálásra kerültek." + }, "typePasskey": { "message": "Hozzáférési kulcs" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Elem részletek" + }, + "itemName": { + "message": "Elem neve" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nem távolíthatók el a csak megtekintési engedéllyel bíró gyűjtemények: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Tulajdonos" + }, + "selfOwnershipLabel": { + "message": "Saját magam", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Feltöltés" + }, + "addAttachment": { + "message": "Melléklet hozzáadása" + }, + "maxFileSizeSansPunctuation": { + "message": "A naximális fájlméret 500 MB." + }, + "deleteAttachmentName": { + "message": "$NAME$ melléklet törlése", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "$NAME$ letöltése", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Biztosan véglegesen törlésre kerüljön ez a melléklet?" + }, + "premium": { + "message": "Prémium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Az ingyenes szervezetek nem használhatnak mellékleteket." + }, "filters": { - "message": "Filters" + "message": "Szűrők" + }, + "cardDetails": { + "message": "Kártyaadatok" + }, + "cardBrandDetails": { + "message": "$BRAND$ adatok", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json index 28e11b71c83..4e92f007942 100644 --- a/apps/browser/src/_locales/id/messages.json +++ b/apps/browser/src/_locales/id/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplikasi Otentikasi" }, - "authenticatorAppDesc": { - "message": "Gunakan aplikasi autentikasi (seperti Authy atau Google Authenticator) untuk menghasilkan kode verifikasi berbasis waktu.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Kunci Keamanan OTP YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Gunakan YubiKey untuk mengakses akun Anda. Bekerja dengan YubiKey 4, 4 Nano, 4C, dan peranti NEO." }, - "duoDesc": { - "message": "Verifikasi dengan Duo Security menggunakan aplikasi Duo Mobile, SMS, panggilan telepon, atau kunci keamanan U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Kode verifikasi akan dikirim via email kepada Anda." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Lingkungan Penyedia Personal" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json index b7830b27ee8..219728ced0a 100644 --- a/apps/browser/src/_locales/it/messages.json +++ b/apps/browser/src/_locales/it/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "App di autenticazione" }, - "authenticatorAppDesc": { - "message": "Usa un'app di autenticazione (come Authy o Google Authenticator) per generare codici di verifica basati sul tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chiave di sicurezza YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Usa YubiKey per accedere al tuo account. Funziona con YubiKey 4, 4 Nano, 4C, e dispositivi NEO." }, - "duoDesc": { - "message": "Verifica con Duo Security usando l'app Duo Mobile, SMS, chiamata telefonica, o chiave di sicurezza U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "I codici di verifica ti saranno inviati per email." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Ambiente self-hosted" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Conferma password del file" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Elementi senza cartella" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "L'organizzazione è disattivata" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Non puoi accedere agli elementi nelle organizzazioni disattivate. Contatta il proprietario della tua organizzazione per ricevere assistenza." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json index a0075901f06..f8ead624a11 100644 --- a/apps/browser/src/_locales/ja/messages.json +++ b/apps/browser/src/_locales/ja/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "認証アプリ" }, - "authenticatorAppDesc": { - "message": "Authy や Google 認証システムなどの認証アプリで時限式の認証コードを生成してください。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Bitwarden Authenticator のような認証アプリによって生成されたコードを入力してください。", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP セキュリティキー" + "yubiKeyTitleV2": { + "message": "Yubico OTP セキュリティキー" }, "yubiKeyDesc": { "message": "YubiKey を使ってアカウントにアクセスできます。 YubiKey 4、4 Nano、4C、NEOに対応しています。" }, - "duoDesc": { - "message": "Duo Mobile アプリや SMS、電話や U2F セキュリティキーを使って Duo Security で認証します。", + "duoDescV2": { + "message": "Duo Security によって生成されたコードを入力してください。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "メールアドレス" }, - "emailDesc": { - "message": "確認コードをメールにお送りします。" + "emailDescV2": { + "message": "メールアドレスに送信されたコードを入力してください。" }, "selfHostedEnvironment": { "message": "セルフホスティング環境" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "ファイルパスワードの確認" }, + "exportSuccess": { + "message": "保管庫データをエクスポートしました" + }, "typePasskey": { "message": "パスキー" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "フォルダーがないアイテム" }, + "itemDetails": { + "message": "アイテムの詳細" + }, + "itemName": { + "message": "アイテム名" + }, + "cannotRemoveViewOnlyCollections": { + "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "組織は無効化されています" }, + "owner": { + "message": "所有者" + }, + "selfOwnershipLabel": { + "message": "あなた", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "無効化された組織のアイテムにアクセスすることはできません。組織の所有者に連絡してください。" }, + "upload": { + "message": "アップロード" + }, + "addAttachment": { + "message": "添付ファイルを追加" + }, + "maxFileSizeSansPunctuation": { + "message": "ファイルサイズの上限は 500MB です" + }, + "deleteAttachmentName": { + "message": "添付ファイル $NAME$ を削除", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "$NAME$ をダウンロード", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "この添付ファイルを完全に削除してもよろしいですか?" + }, + "premium": { + "message": "プレミアム" + }, + "freeOrgsCannotUseAttachments": { + "message": "無料の組織は添付ファイルを使用できません" + }, "filters": { "message": "フィルター" + }, + "cardDetails": { + "message": "カード情報" + }, + "cardBrandDetails": { + "message": "$BRAND$ の詳細", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json index b848a578063..4c2ca642151 100644 --- a/apps/browser/src/_locales/ka/messages.json +++ b/apps/browser/src/_locales/ka/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/km/messages.json +++ b/apps/browser/src/_locales/km/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json index d38a8d7ae76..ebc97ca1220 100644 --- a/apps/browser/src/_locales/kn/messages.json +++ b/apps/browser/src/_locales/kn/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್" }, - "authenticatorAppDesc": { - "message": "ಸಮಯ ಆಧಾರಿತ ಪರಿಶೀಲನಾ ಕೋಡ್‌ಗಳನ್ನು ರಚಿಸಲು ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಬಳಸಿ (ಆಥಿ ಅಥವಾ ಗೂಗಲ್ ಅಥೆಂಟಿಕೇಟರ್).", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "ಯುಬಿಕೆ ಒಟಿಪಿ ಭದ್ರತಾ ಕೀ" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಯುಬಿಕೆ ಬಳಸಿ. ಯುಬಿಕೆ 4, 4 ನ್ಯಾನೋ, 4 ಸಿ ಮತ್ತು ಎನ್ಇಒ ಸಾಧನಗಳೊಂದಿಗೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ." }, - "duoDesc": { - "message": "ಡ್ಯುಯೊ ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಯು 2 ಎಫ್ ಭದ್ರತಾ ಕೀಲಿಯನ್ನು ಬಳಸಿಕೊಂಡು ಡ್ಯುಯೊ ಸೆಕ್ಯುರಿಟಿಯೊಂದಿಗೆ ಪರಿಶೀಲಿಸಿ.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ಇಮೇಲ್" }, - "emailDesc": { - "message": "ಪರಿಶೀಲನೆ ಕೋಡ್‌ಗಳನ್ನು ನಿಮಗೆ ಇಮೇಲ್ ಮಾಡಲಾಗುತ್ತದೆ." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "ಸ್ವಯಂ ಆತಿಥೇಯ ಪರಿಸರ" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json index a7e5ce201a6..5b33655c21a 100644 --- a/apps/browser/src/_locales/ko/messages.json +++ b/apps/browser/src/_locales/ko/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "인증 앱" }, - "authenticatorAppDesc": { - "message": "인증 앱(Authy, Google OTP 등)을 통하여 일회용 인증 코드를 생성합니다.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 보안 키" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "YubiKey를 사용하여 사용자의 계정에 접근합니다. YubiKey 4, 4 Nano, 4C 및 NEO 기기를 사용할 수 있습니다." }, - "duoDesc": { - "message": "Duo Mobile 앱, SMS, 전화 통화를 사용한 Duo Security 또는 U2F 보안 키를 사용하여 인증하세요.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "이메일" }, - "emailDesc": { - "message": "인증 코드가 담긴 이메일을 다시 보냅니다." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "자체 호스팅 환경" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "패스키" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json index c9e43d50556..e773c9a83b0 100644 --- a/apps/browser/src/_locales/lt/messages.json +++ b/apps/browser/src/_locales/lt/messages.json @@ -50,7 +50,7 @@ "message": "Pagrindinio slaptažodžio užuomina gali padėti Jums prisiminti slaptažodį, jei jį pamiršite." }, "masterPassHintText": { - "message": "Jei pamiršote savo slaptažodį, slaptažodžio užuomina gali būti išsiūstas į jūsų el. pašto adresą. $CURRENT$/$MAXIMUM$ simbolių.", + "message": "Jei pamiršote slaptažodį, slaptažodžio užuomina gali būti išsiųsta į jūsų el. paštą. $CURRENT$ / $MAXIMUM$ didžiausias simbolių skaičius.", "placeholders": { "current": { "content": "$1", @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentifikavimo programa" }, - "authenticatorAppDesc": { - "message": "Naudok autentifikatoriaus programėlę (pvz., Authy arba Google Autentifikatorius), kad sugeneruotum laiko patikrinimo kodus.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Įveskite autentifikatoriaus programėlės sugeneruotą kodą, pvz., „Bitwarden Authenticator“.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP saugumo raktas" + "yubiKeyTitleV2": { + "message": "„Yubico“ OTP saugumo raktas" }, "yubiKeyDesc": { "message": "Naudok YubiKey, kad prisijungtum prie savo paskyros. Veikia su YubiKey 4, 4 Nano, 4C ir NEO įrenginiais." }, - "duoDesc": { - "message": "Patvirtink su Duo Security naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", + "duoDescV2": { + "message": "Įveskite „Duo Security“ sugeneruotą kodą.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "El. paštas" }, - "emailDesc": { - "message": "Patvirtinimo kodai bus atsiųsti el. paštu tau." + "emailDescV2": { + "message": "Įveskite į el. paštą atsiųstą kodą." }, "selfHostedEnvironment": { "message": "Savarankiškai sukurta aplinka" @@ -2890,7 +2890,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Perjungti šoninę naršymą" }, "skipToContent": { "message": "Pereiti prie turinio" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Patvirtinti failo slaptažodį" }, + "exportSuccess": { + "message": "Eksportuoti saugyklos duomenys" + }, "typePasskey": { "message": "Prieigos raktas" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Įkelti" + }, + "addAttachment": { + "message": "Pridėti priedą" + }, + "maxFileSizeSansPunctuation": { + "message": "Didžiausias failo dydis – 500 MB" + }, + "deleteAttachmentName": { + "message": "Ištrinti priedą $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Atsisiųsti $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Ar tikrai norite negrįžtamai ištrinti šį priedą?" + }, + "premium": { + "message": "„Premium“" + }, + "freeOrgsCannotUseAttachments": { + "message": "Nemokamos organizacijos negali naudoti priedų" + }, "filters": { "message": "Filtrai" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json index 6ddfda8d57b..633f8069826 100644 --- a/apps/browser/src/_locales/lv/messages.json +++ b/apps/browser/src/_locales/lv/messages.json @@ -50,7 +50,7 @@ "message": "Galvenās paroles norāde var palīdzēt atcerēties paroli, ja tā ir aizmirsta." }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Ja tiks aizmirsta parole, tās norādi var nosūtīt uz e-pasta adresi. $CURRENT$/$MAXIMUM$ lielākais pieļaujamais rakstzīmju skaits.", "placeholders": { "current": { "content": "$1", @@ -183,7 +183,7 @@ "message": "Apstiprināšanas kods" }, "confirmIdentity": { - "message": "Jāapstiprina identitāte, lai turpinātu." + "message": "Jāapliecina sava identitāte, lai turpinātu." }, "changeMasterPassword": { "message": "Mainīt galveno paroli" @@ -478,10 +478,10 @@ "message": "Pārlūks neatbalsta vienkāršo ievietošanu starpliktuvē. Tā vietā tas jāievieto starpliktuvē pašrocīgi." }, "verifyIdentity": { - "message": "Apstiprināt identitāti" + "message": "Identitātes apliecināšana" }, "yourVaultIsLocked": { - "message": "Glabātava ir aizslēgta. Jāapstiprina identitāte, lai turpinātu." + "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, "unlock": { "message": "Atslēgt" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentificētāja lietotne" }, - "authenticatorAppDesc": { - "message": "Izmanto autentificētāja lietotni (piemēram, Authy vai Google autentifikators), lai izveidotu laikā balstītus apstiprinājuma kodus!", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Jāievada autentificētāja lietotnes, piemēram, Bitwarden Authenticator, izveidots kods.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP drošības atslēga" + "yubiKeyTitleV2": { + "message": "Yubico OTP drošības atslēga" }, "yubiKeyDesc": { "message": "Ir izmantojams YubiKey, lai piekļūtu savam kontam. Darbojas ar YubiKey 4, 4 Nano, 4C un NEO ierīcēm." }, - "duoDesc": { - "message": "Ar Duo Security apliecināšanu var veikt ar Duo Mobile lietotni, īsziņu, tālruņa zvanu vai U2F drošības atslēgu.", + "duoDescV2": { + "message": "Jāievada Duo Security izveidots kods.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-pasts" }, - "emailDesc": { - "message": "Apstiprinājuma kodi tiks nosūtīti e-pastā." + "emailDescV2": { + "message": "Jāievada e-pastā nosūtītais kods." }, "selfHostedEnvironment": { "message": "Pašuzturēta vide" @@ -2159,7 +2159,7 @@ "message": "Galvenās paroles apstiprināšana" }, "passwordConfirmationDesc": { - "message": "Šī darbība ir aizsargāta. Lai turpinātu, ir jāievada galvenā parole, lai apstiprinātu identitāti." + "message": "Šī darbība ir aizsargāta. Lai turpinātu, ir jāievada galvenā parole, lai apliecinātu savu identitāti." }, "emailVerificationRequired": { "message": "Nepieciešama e-pasta adreses apstiprināšana" @@ -2890,7 +2890,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Pārslēgt sānu pārvietošanās joslu" }, "skipToContent": { "message": "Pāriet uz saturu" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Apstiprināt datnes paroli" }, + "exportSuccess": { + "message": "Glabātavas saturs izgūts" + }, "typePasskey": { "message": "Piekļuves atslēga" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Vienumi bez mapes" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Apvienība ir atspējota" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Atspējotu apvienību vienumiem nevar piekļūt. Jāsazinās ar apvienības īpašnieku, lai iegūtu palīdzību." }, + "upload": { + "message": "Augšupielādēt" + }, + "addAttachment": { + "message": "Pievienot pielikumu" + }, + "maxFileSizeSansPunctuation": { + "message": "Lielākais pieļaujamais datnes izmērs ir 500 MB" + }, + "deleteAttachmentName": { + "message": "Izdzēst pielikumu $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Lejupielādēt $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Vai tiešām neatgriezeniski izdzēst šo pielikumu?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Bezmaksas apvienības nevar izmantot pielikumus" + }, "filters": { "message": "Atlases" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json index 9f99b4f15ee..81c3608d10a 100644 --- a/apps/browser/src/_locales/ml/messages.json +++ b/apps/browser/src/_locales/ml/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "ഓതന്റിക്കേറ്റർ ആപ്പ്" }, - "authenticatorAppDesc": { - "message": "സമയ-അടിസ്ഥാന പരിശോധന കോഡുകൾ സൃഷ്ടിക്കുന്നതിന് ഒരു ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ (ഓത്തി അല്ലെങ്കിൽ Google ഓതന്റിക്കേറ്റർ പോലുള്ളവ) ഉപയോഗിക്കുക.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP സുരക്ഷാ കീ" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "നിങ്ങളുടെ അക്കൗണ്ട് ആക്സസ് ചെയ്യുന്നതിന് ഒരു യൂബിക്കി ഉപയോഗിക്കുക. YubiKey 4, 4 Nano, 4C, NEO ഉപകരണങ്ങളിൽ പ്രവർത്തിക്കുന്നു." }, - "duoDesc": { - "message": "Duo Mobile അപ്ലിക്കേഷൻ, എസ്എംഎസ്, ഫോൺ കോൾ അല്ലെങ്കിൽ യു 2 എഫ് സുരക്ഷാ കീ ഉപയോഗിച്ച് Duoസെക്യൂരിറ്റി ഉപയോഗിച്ച് പരിശോധിക്കുക.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ഇമെയിൽ" }, - "emailDesc": { - "message": "സ്ഥിരീകരണ കോഡുകൾ നിങ്ങൾക്ക് ഇമെയിൽ ചെയ്യും." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "സ്വയം ഹോസ്റ്റുചെയ്‌ത എൻവിയോണ്മെന്റ്" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json index 7916e5cfe1a..73b928ebe0c 100644 --- a/apps/browser/src/_locales/mr/messages.json +++ b/apps/browser/src/_locales/mr/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/my/messages.json +++ b/apps/browser/src/_locales/my/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json index c6ed8beb6bf..3c07e43ff96 100644 --- a/apps/browser/src/_locales/nb/messages.json +++ b/apps/browser/src/_locales/nb/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentiseringsapp" }, - "authenticatorAppDesc": { - "message": "Bruk en autentiseringsapp (f.eks. Authy eller Google Authenticator) for å generere tidsbegrensede verifiseringskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP-sikkerhetsnøkkel" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Bruk en YubiKey for å få tilgang til kontoen din. Virker med enheter av typene YubiKey 4, 4 Nano, 4C, og NEO." }, - "duoDesc": { - "message": "Verifiser med Duo Security gjennom Duo Mobile-appen, SMS, telefonsamtale, eller en U2F-sikkerhetsnøkkel.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Verifiseringskoder vil bli sendt til deg med E-post." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Selvbetjent miljø" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Bekreft filpassord" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/ne/messages.json +++ b/apps/browser/src/_locales/ne/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json index ddb1313c4ad..98cd9ca449d 100644 --- a/apps/browser/src/_locales/nl/messages.json +++ b/apps/browser/src/_locales/nl/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticatie-app" }, - "authenticatorAppDesc": { - "message": "Gebruik een authenticatie-app (zoals Authy of Google Authenticator) om tijdgebaseerde authenticatiecodes te genereren.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Door een authenticatie-app zoals Bitwarden Authenticator gegenereerde code invoeren.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "YubiKey OTP-beveiligingssleutel" }, "yubiKeyDesc": { "message": "Gebruik een YubiKey om toegang te krijgen tot uw account. Werkt met YubiKey 4, 4 Nano, 4C en Neo-apparaten." }, - "duoDesc": { - "message": "Verificatie met Duo Security middels de Duo Mobile-app, sms, spraakoproep of een U2F-beveiligingssleutel.", + "duoDescV2": { + "message": "Door Duo Security gegenereerde code invoeren.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Je ontvangt verificatiecodes via e-mail." + "emailDescV2": { + "message": "Via e-mail verstuurde code invoeren." }, "selfHostedEnvironment": { "message": "Zelfgehoste omgeving" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Bestandswachtwoord bevestigen" }, + "exportSuccess": { + "message": "Kluisgegevens geëxporteerd" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items zonder map" }, + "itemDetails": { + "message": "Itemdetails" + }, + "itemName": { + "message": "Itemnaam" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organisatie is gedeactiveerd" }, + "owner": { + "message": "Eigenaar" + }, + "selfOwnershipLabel": { + "message": "Jij", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in een gedeactiveerde organisatie zijn niet toegankelijk. Neem contact op met de eigenaar van je organisatie voor hulp." }, + "upload": { + "message": "Uploaden" + }, + "addAttachment": { + "message": "Bijlage toevoegen" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximale bestandsgrootte is 500 MB" + }, + "deleteAttachmentName": { + "message": "Bijlage $NAME$ verwijderen", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "$NAME$ downloaden", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Weet je zeker dat je deze bijlage definitief permanen wilt verwijderen?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Gratis organisaties kunnen geen bijlagen gebruiken" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Kaartgegevens" + }, + "cardBrandDetails": { + "message": "", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/nn/messages.json +++ b/apps/browser/src/_locales/nn/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/or/messages.json +++ b/apps/browser/src/_locales/or/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json index 60792df2a41..40ba6659fb0 100644 --- a/apps/browser/src/_locales/pl/messages.json +++ b/apps/browser/src/_locales/pl/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplikacja uwierzytelniająca" }, - "authenticatorAppDesc": { - "message": "Użyj aplikacji mobilnej (np. Authy lub Google Authenticator) do generowania czasowych kodów weryfikacyjnych.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Wprowadź kod wygenerowany przez aplikację uwierzytelniającą, jak Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Klucz bezpieczeństwa YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Klucz bezpieczeństwa Yubico OTP" }, "yubiKeyDesc": { "message": "Użyj YubiKey jako metody dostępu do konta. Działa z YubiKey 4, 4 Nano, 4C i urządzeniami NEO." }, - "duoDesc": { - "message": "Weryfikacja z użyciem Duo Security poprzez aplikację Duo Mobile, SMS, połączenie telefoniczne lub klucz bezpieczeństwa U2F.", + "duoDescV2": { + "message": "Wprowadź kod wygenerowany przez Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Adres e-mail" }, - "emailDesc": { - "message": "Kody weryfikacyjne zostaną wysłane do Ciebie wiadomością e-mail." + "emailDescV2": { + "message": "Wpisz kod wysłany na Twój adres e-mail." }, "selfHostedEnvironment": { "message": "Samodzielnie hostowane środowisko" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Potwierdź hasło pliku" }, + "exportSuccess": { + "message": "Dane sejfu zostały wyeksportowane" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Elementy bez folderu" }, + "itemDetails": { + "message": "Szczegóły elementu" + }, + "itemName": { + "message": "Nazwa elementu" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organizacja jest wyłączona" }, + "owner": { + "message": "Właściciel" + }, + "selfOwnershipLabel": { + "message": "Ty", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Nie można uzyskać dostępu do elementów w wyłączonych organizacjach. Skontaktuj się z właścicielem organizacji, aby uzyskać pomoc." }, + "upload": { + "message": "Wyślij" + }, + "addAttachment": { + "message": "Dodaj załącznik" + }, + "maxFileSizeSansPunctuation": { + "message": "Maksymalny rozmiar pliku to 500 MB" + }, + "deleteAttachmentName": { + "message": "Usuń załącznik $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Pobierz $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Czy na pewno chcesz trwale usunąć ten załącznik?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Darmowe organizacje nie mogą używać załączników" + }, "filters": { "message": "Filtry" + }, + "cardDetails": { + "message": "Szczegóły karty" + }, + "cardBrandDetails": { + "message": "Szczegóły $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json index 94c1e349bb7..76dd0025851 100644 --- a/apps/browser/src/_locales/pt_BR/messages.json +++ b/apps/browser/src/_locales/pt_BR/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicativo de Autenticação" }, - "authenticatorAppDesc": { - "message": "Utilize um aplicativo de autenticação (tal como Authy ou Google Authenticator) para gerar códigos de verificação baseados no tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chave de Segurança YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para acessar a sua conta. Funciona com YubiKey 4, 4 Nano, 4C, e dispositivos NEO." }, - "duoDesc": { - "message": "Verifique com o Duo Security utilizando o aplicativo Duo Mobile, SMS, chamada telefônica, ou chave de segurança U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Os códigos de verificação vão ser enviados por e-mail para você." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Ambiente Auto-hospedado" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirmar senha do arquivo" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Chave de acesso" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Itens sem pasta" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "A organização está desativada" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Itens em organizações desativadas não podem ser acessados. Entre em contato com o proprietário da sua organização para obter assistência." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json index 882f33db0ab..5578ac37a6e 100644 --- a/apps/browser/src/_locales/pt_PT/messages.json +++ b/apps/browser/src/_locales/pt_PT/messages.json @@ -239,10 +239,10 @@ "message": "Bitwarden para Empresas" }, "bitwardenAuthenticator": { - "message": "Autenticador Bitwarden" + "message": "Bitwarden Authenticator" }, "continueToAuthenticatorPageDesc": { - "message": "O Autenticador Bitwarden permite-lhe armazenar chaves de autenticação e gerar códigos TOTP para fluxos de verificação de 2 passos. Saiba mais no site bitwarden.com" + "message": "O Bitwarden Authenticator permite-lhe armazenar chaves de autenticação e gerar códigos TOTP para fluxos de verificação de 2 passos. Saiba mais no site bitwarden.com" }, "bitwardenSecretsManager": { "message": "Gestor de Segredos Bitwarden" @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicação de autenticação" }, - "authenticatorAppDesc": { - "message": "Utilize uma aplicação de autenticação (como o Authy ou o Google Authenticator) para gerar códigos de verificação baseados no tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Introduza um código gerado por uma aplicação de autenticação como o Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chave de segurança YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Chave de segurança Yubico OTP" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para aceder à sua conta. Funciona com os dispositivos YubiKey 4, 4 Nano, 4C e NEO." }, - "duoDesc": { - "message": "Verifique com a Duo Security utilizando a aplicação Duo Mobile, SMS, chamada telefónica ou chave de segurança U2F.", + "duoDescV2": { + "message": "Introduza um código gerado pelo Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Os códigos de verificação ser-lhe-ão enviados por e-mail." + "emailDescV2": { + "message": "Introduza um código enviado para o seu e-mail." }, "selfHostedEnvironment": { "message": "Ambiente auto-hospedado" @@ -1382,7 +1382,7 @@ "message": "Nome próprio" }, "middleName": { - "message": "Segundo nome" + "message": "Nome do meio" }, "lastName": { "message": "Apelido" @@ -1391,7 +1391,7 @@ "message": "Nome completo" }, "identityName": { - "message": "Nome de identidade" + "message": "Nome da identidade" }, "company": { "message": "Empresa" @@ -1642,7 +1642,7 @@ "message": "Desbloquear com PIN" }, "setYourPinCode": { - "message": "Defina o seu código PIN para desbloquear o Bitwarden. As suas definições de PIN serão redefinidas se alguma vez terminar sessão completamente da aplicação." + "message": "Defina o seu código PIN para desbloquear o Bitwarden. As suas definições de PIN serão redefinidas se alguma vez terminar sessão por completo da aplicação." }, "pinRequired": { "message": "É necessário o código PIN." @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirmar a palavra-passe do ficheiro" }, + "exportSuccess": { + "message": "Dados do cofre exportados" + }, "typePasskey": { "message": "Chave de acesso" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Itens sem pasta" }, + "itemDetails": { + "message": "Detalhes do item" + }, + "itemName": { + "message": "Nome do item" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "A organização está desativada" }, + "owner": { + "message": "Proprietário" + }, + "selfOwnershipLabel": { + "message": "Eu", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Não é possível aceder aos itens de organizações desativadas. Contacte o proprietário da organização para obter assistência." }, + "upload": { + "message": "Carregar" + }, + "addAttachment": { + "message": "Adicionar anexo" + }, + "maxFileSizeSansPunctuation": { + "message": "O tamanho máximo do ficheiro é de 500 MB" + }, + "deleteAttachmentName": { + "message": "Eliminar o anexo $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Transferir $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Tem a certeza de que pretende eliminar permanentemente este anexo?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "As organizações gratuitas não podem utilizar anexos" + }, "filters": { "message": "Filtros" + }, + "cardDetails": { + "message": "Detalhes do cartão" + }, + "cardBrandDetails": { + "message": "Detalhes do $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json index 055592fa093..02e92106c44 100644 --- a/apps/browser/src/_locales/ro/messages.json +++ b/apps/browser/src/_locales/ro/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplicația Authenticator" }, - "authenticatorAppDesc": { - "message": "Utilizați o aplicație de autentificare (cum ar fi Authy sau Google Authenticator) pentru a genera codurile de verificare bazate pe timp.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Cheie de securitate YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Utilizați YubiKey pentru a accesa contul dvs. Funcționează cu dispozitivele YubiKey 4, 4 Nano, 4C și NEO." }, - "duoDesc": { - "message": "Verificați cu Duo Security utilizând aplicația Duo Mobile, SMS, apel telefonic sau cheia de securitate U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Codurile de verificare vor fi trimise prin e-mail." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Mediu autogăzduit" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json index bc9ae9fd7f5..bb9a030a6fa 100644 --- a/apps/browser/src/_locales/ru/messages.json +++ b/apps/browser/src/_locales/ru/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Приложение-аутентификатор" }, - "authenticatorAppDesc": { - "message": "Используйте приложение-аутентификатор (например, Authy или Google Authenticator) для создания кодов проверки на основе времени.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Введите код, сгенерированный приложением-аутентификатором, например Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ безопасности YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Ключ безопасности Yubico OTP" }, "yubiKeyDesc": { "message": "Используйте YubiKey для доступа к вашей учетной записи. Работает с устройствами YubiKey 4 серии, 5 серии и NEO." }, - "duoDesc": { - "message": "Подтвердите с помощью Duo Security, используя приложение Duo Mobile, SMS, телефонный звонок или ключ безопасности U2F.", + "duoDescV2": { + "message": "Введите код, сгенерированный Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Коды подтверждения будут отправлены вам по электронной почте." + "emailDescV2": { + "message": "Введите код, отправленный на ваш email." }, "selfHostedEnvironment": { "message": "Окружение пользовательского хостинга" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Подтвердите пароль к файлу" }, + "exportSuccess": { + "message": "Данные хранилища экспортированы" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Элементы без папки" }, + "itemDetails": { + "message": "Информация об элементе" + }, + "itemName": { + "message": "Название элемента" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Организация деактивирована" }, + "owner": { + "message": "Владелец" + }, + "selfOwnershipLabel": { + "message": "Вы", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Доступ к элементам в деактивированных организациях невозможен. Обратитесь за помощью к владельцу организации." }, + "upload": { + "message": "Загрузить" + }, + "addAttachment": { + "message": "Добавить вложение" + }, + "maxFileSizeSansPunctuation": { + "message": "Максимальный размер файла 500 МБ" + }, + "deleteAttachmentName": { + "message": "Удалить вложение $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Скачать $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Вы уверены, что хотите навсегда удалить это вложение?" + }, + "premium": { + "message": "Премиум" + }, + "freeOrgsCannotUseAttachments": { + "message": "Бесплатные организации не могут использовать вложения" + }, "filters": { - "message": "Filters" + "message": "Фильтры" + }, + "cardDetails": { + "message": "Реквизиты карты" + }, + "cardBrandDetails": { + "message": "Реквизиты $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json index 71d56b07f70..0da68c198cd 100644 --- a/apps/browser/src/_locales/si/messages.json +++ b/apps/browser/src/_locales/si/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "සත්යාපන යෙදුම" }, - "authenticatorAppDesc": { - "message": "කාලය මත පදනම් වූ සත්යාපන කේත ජනනය කිරීම සඳහා සත්යාපන යෙදුමක් (සත්යාපන හෝ ගූගල් සත්යාපන වැනි) භාවිතා කරන්න.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP ආරක්ෂක යතුර" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "ඔබගේ ගිණුමට ප්රවේශ වීමට YuBiKey භාවිතා කරන්න. YuBiKey 4, 4 නැනෝ, 4C, සහ NEO උපාංග සමඟ ක්රියා කරයි." }, - "duoDesc": { - "message": "Duo ජංගම යෙදුම, කෙටි පණිවුඩ, දුරකථන ඇමතුමක්, හෝ U2F ආරක්ෂක යතුර භාවිතා කරමින් Duo ආරක්ෂක සමඟ තහවුරු කරන්න.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "ඊ-තැපැල්" }, - "emailDesc": { - "message": "සත්යාපන කේත ඔබට විද්යුත් තැපැල් කරනු ඇත." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "ස්වයං සත්කාරක පරිසරය" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json index 6dcc031bdfb..d190bb1925d 100644 --- a/apps/browser/src/_locales/sk/messages.json +++ b/apps/browser/src/_locales/sk/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Overovacia aplikácia" }, - "authenticatorAppDesc": { - "message": "Použite overovaciu aplikáciu (napríklad Authy alebo Google Authenticator) na generovanie časovo obmedzených overovacích kódov.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Zadajte kód vygenerovaný overovacou aplikáciou akou je napríklad Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP bezpečnostný kľúč" + "yubiKeyTitleV2": { + "message": "Bezpečnostný kľúč Yubico OTP" }, "yubiKeyDesc": { "message": "Použiť YubiKey pre prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." }, - "duoDesc": { - "message": "Overiť s Duo Security použitím Duo Mobile aplikácie, SMS, telefonátu alebo U2F bezpečnostným kľúčom.", + "duoDescV2": { + "message": "Zadajte kód vygenerovaný aplikáciou Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verifikačné kódy vám budú zaslané emailom." + "emailDescV2": { + "message": "Zadajte kód zaslaný na váš e-mail." }, "selfHostedEnvironment": { "message": "Sebou hosťované prostredie" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Potvrdiť heslo súboru" }, + "exportSuccess": { + "message": "Údaje z trezora boli exportované" + }, "typePasskey": { "message": "Prístupový kľúč" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Položky bez priečinka" }, + "itemDetails": { + "message": "Podrobnosti o položke" + }, + "itemName": { + "message": "Názov položky" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organizácia je vypnutá" }, + "owner": { + "message": "Vlastník" + }, + "selfOwnershipLabel": { + "message": "Vy", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "K položkám vo vypnutej organizácii nie je možné pristupovať. Požiadajte o pomoc vlastníka organizácie." }, + "upload": { + "message": "Nahrať" + }, + "addAttachment": { + "message": "Priložiť prílohu" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximálna veľkosť súboru je 500 MB" + }, + "deleteAttachmentName": { + "message": "Odstrániť prílohu $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Stiahnuť $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Naozaj chcete natrvalo odstrániť túto prílohu?" + }, + "premium": { + "message": "Prémium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Bezplatné organizácie nemôžu používať prílohy" + }, "filters": { - "message": "Filters" + "message": "Filtre" + }, + "cardDetails": { + "message": "Podrobnosti o karte" + }, + "cardBrandDetails": { + "message": "Podrobnosti o $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json index e13b1d71727..39ab221327b 100644 --- a/apps/browser/src/_locales/sl/messages.json +++ b/apps/browser/src/_locales/sl/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Aplikacija za avtentikacijo" }, - "authenticatorAppDesc": { - "message": "Uporabite aplikacijo za avtentikacijo (npr. Authy ali Google Authenticator), ki za vas ustvarja časovno spremenljive kode.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Varnostni ključ YubiKey za enkratna gesla" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Za dostop do svojega računa uporabite YubiKey. Podprti so YubiKey 4, 4 Nano, 4C in naprave NEO." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-pošta" }, - "emailDesc": { - "message": "Potrditvene kode vam bodo posredovane po e-pošti." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json index 9820cea4616..ac20e402f10 100644 --- a/apps/browser/src/_locales/sr/messages.json +++ b/apps/browser/src/_locales/sr/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Апликација Аутентификатор" }, - "authenticatorAppDesc": { - "message": "Користите апликацију за аутентификацију (као што је Authy или Google Authenticator) за генерисање верификационих кодова.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Унесите кôд који генерише апликација за аутентификацију као што је Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP сигурносни кључ" + "yubiKeyTitleV2": { + "message": "Yubico OTP сигурносни кључ" }, "yubiKeyDesc": { "message": "Користите YubiKey за приступ налогу. Ради са YubiKey 4, 4 Nano, 4C, и NEO уређајима." }, - "duoDesc": { - "message": "Провери са Duo Security користећи Duo Mobile апликацију, СМС, телефонски позив, или U2F кључ.", + "duoDescV2": { + "message": "Унесите кôд који је генерисао Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Е-пошта" }, - "emailDesc": { - "message": "Верификациони кодови ће вам бити послати имејлом." + "emailDescV2": { + "message": "Унесите кôд послат на ваш имејл." }, "selfHostedEnvironment": { "message": "Самостално окружење" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Потврдити лозинку датотеке" }, + "exportSuccess": { + "message": "Подаци из сефа су извезени" + }, "typePasskey": { "message": "Приступачни кључ" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Ставке без фасцикле" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Организација је деактивирана" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Није могуће приступити ставкама у деактивираним организацијама. Обратите се власнику ваше организације за помоћ." }, + "upload": { + "message": "Отпреми" + }, + "addAttachment": { + "message": "Додај прилог" + }, + "maxFileSizeSansPunctuation": { + "message": "Максимална величина је 500МБ" + }, + "deleteAttachmentName": { + "message": "Обриши прилог $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Преузми $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Да ли сте сигурни да желите да трајно избришете овај прилог?" + }, + "premium": { + "message": "Премијум" + }, + "freeOrgsCannotUseAttachments": { + "message": "Бесплатне организације не могу да користе прилоге" + }, "filters": { - "message": "Filters" + "message": "Филтери" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json index 200db544ac9..dc351bc6878 100644 --- a/apps/browser/src/_locales/sv/messages.json +++ b/apps/browser/src/_locales/sv/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Autentiseringsapp" }, - "authenticatorAppDesc": { - "message": "Använd en autentiseringsapp (till exempel Authy eller Google Authenticator) för att skapa tidsbaserade verifieringskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP säkerhetsnyckel" + "yubiKeyTitleV2": { + "message": "Yubico OTP-säkerhetsnyckel" }, "yubiKeyDesc": { "message": "Använd en YubiKey för att få åtkomst till ditt konto. Fungerar med YubiKey 4, 4 Nano, 4C och NEO enheter." }, - "duoDesc": { - "message": "Verifiera med Duo Security genom att använda Duo Mobile-appen, SMS, telefonsamtal eller en U2F säkerhetsnyckel.", + "duoDescV2": { + "message": "Ange en kod som genererats av Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Verifieringskoder kommer att skickas till dig via e-post." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Egen-hostad miljö" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Bekräfta fillösenord" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Lösennyckel" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Objekt utan mapp" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Objektnamn" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Ägare" + }, + "selfOwnershipLabel": { + "message": "Du", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Ladda upp" + }, + "addAttachment": { + "message": "Lägg till bilaga" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximal filstorlek är 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Ladda ner $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json index e4fc9c23f01..d1412681ac2 100644 --- a/apps/browser/src/_locales/te/messages.json +++ b/apps/browser/src/_locales/te/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json index 3074053619a..a1e8479da87 100644 --- a/apps/browser/src/_locales/th/messages.json +++ b/apps/browser/src/_locales/th/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Authenticator App" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "อีเมล" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Self-hosted Environment" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json index 4b630226b24..20114a5fd8e 100644 --- a/apps/browser/src/_locales/tr/messages.json +++ b/apps/browser/src/_locales/tr/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Kimlik doğrulama uygulaması" }, - "authenticatorAppDesc": { - "message": "Zamana dayalı doğrulama kodları oluşturmak için kimlik doğrulama uygulaması (örn. Authy veya Google Authenticator) kullanın.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP güvenlik anahtarı" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Hesabınıza erişmek için bir YubiKey kullanın. YubiKey 4, 4 Nano, 4C ve NEO cihazlarıyla çalışır." }, - "duoDesc": { - "message": "Duo Security ile doğrulama için Duo Mobile uygulaması, SMS, telefon araması veya U2F güvenlik anahtarını kullanın.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "E-posta" }, - "emailDesc": { - "message": "Doğrulama kodları e-posta adresinize gönderilecek." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Şirket içinde barındırılan ortam" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Dosya parolasını onaylayın" }, + "exportSuccess": { + "message": "Kasa verileri dışa aktarıldı" + }, "typePasskey": { "message": "Geçiş anahtarı" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Klasörü olmayan kayıtlar" }, + "itemDetails": { + "message": "Kayıt ayrıntıları" + }, + "itemName": { + "message": "Kayıt adı" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Kuruluş pasifleştirilmiş" }, + "owner": { + "message": "Sahibi" + }, + "selfOwnershipLabel": { + "message": "Siz", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Pasif kuruluşlardaki kayıtlara erişilemez. Destek almak için kuruluş sahibinizle iletişime geçin." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filtreler" + }, + "cardDetails": { + "message": "Kart bilgileri" + }, + "cardBrandDetails": { + "message": "$BRAND$ bilgileri", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json index 4621b7a916a..bcb552ee19b 100644 --- a/apps/browser/src/_locales/uk/messages.json +++ b/apps/browser/src/_locales/uk/messages.json @@ -472,7 +472,7 @@ "message": "Оцінити розширення" }, "rateExtensionDesc": { - "message": "Будь ласка, подумайте про те, щоб допомогти нам хорошим відгуком!" + "message": "Розкажіть іншим про свої враження, залишивши хороший відгук!" }, "browserNotSupportClipboard": { "message": "Ваш браузер не підтримує копіювання даних в буфер обміну. Скопіюйте вручну." @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Програма автентифікації" }, - "authenticatorAppDesc": { - "message": "Використовуйте програму автентифікації (наприклад, Authy або Google Authenticator), щоб генерувати тимчасові коди підтвердження.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Введіть код, згенерований програмою для автентифікації, як-от Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ безпеки YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Ключ безпеки Yubico OTP" }, "yubiKeyDesc": { "message": "Використовуйте YubiKey для доступу до сховища. Працює з YubiKey 4, 4 Nano, 4C та пристроями NEO." }, - "duoDesc": { - "message": "Авторизуйтесь за допомогою Duo Security з використанням мобільного додатку Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", + "duoDescV2": { + "message": "Введіть код, згенерований Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Е-пошта" }, - "emailDesc": { - "message": "Коди підтвердження будуть надсилатися на вашу пошту." + "emailDescV2": { + "message": "Введіть код, надісланий вам електронною поштою." }, "selfHostedEnvironment": { "message": "Середовище власного хостингу" @@ -1163,7 +1163,7 @@ "message": "URL-адреса сервера API" }, "webVaultUrl": { - "message": "URL-адреса сервера веб сховища" + "message": "URL-адреса сервера вебсховища" }, "identityUrl": { "message": "URL-адреса сервера ідентифікації" @@ -2059,7 +2059,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "За бажанням вимагати пароль в користувачів для доступу до цього відправлення.", + "message": "Ви можете встановити пароль для доступу до цього відправлення.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2165,7 +2165,7 @@ "message": "Необхідно підтвердити е-пошту" }, "emailVerificationRequiredDesc": { - "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у веб сховищі." + "message": "Для використання цієї функції необхідно підтвердити електронну пошту. Ви можете виконати підтвердження у вебсховищі." }, "updatedMasterPassword": { "message": "Головний пароль оновлено" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Підтвердьте пароль файлу" }, + "exportSuccess": { + "message": "Дані сховища експортовано" + }, "typePasskey": { "message": "Ключ доступу" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Елементи без теки" }, + "itemDetails": { + "message": "Подробиці запису" + }, + "itemName": { + "message": "Назва запису" + }, + "cannotRemoveViewOnlyCollections": { + "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Організацію деактивовано" }, + "owner": { + "message": "Власник" + }, + "selfOwnershipLabel": { + "message": "Ви", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Елементи в деактивованих організаціях недоступні. Зверніться до власника вашої організації для отримання допомоги." }, + "upload": { + "message": "Вивантажити" + }, + "addAttachment": { + "message": "Додати вкладення" + }, + "maxFileSizeSansPunctuation": { + "message": "Максимальний розмір файлу – 500 МБ" + }, + "deleteAttachmentName": { + "message": "Видалити вкладення $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Завантажити $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Ви дійсно хочете остаточно видалити це вкладення?" + }, + "premium": { + "message": "Преміум" + }, + "freeOrgsCannotUseAttachments": { + "message": "Організації без передплати не можуть використовувати вкладення" + }, "filters": { - "message": "Filters" + "message": "Фільтри" + }, + "cardDetails": { + "message": "Подробиці картки" + }, + "cardBrandDetails": { + "message": "Подробиці $BRAND$", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json index 2af7d82f8d1..c63e571076c 100644 --- a/apps/browser/src/_locales/vi/messages.json +++ b/apps/browser/src/_locales/vi/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "Ứng dụng Authenticator" }, - "authenticatorAppDesc": { - "message": "Sử dụng một ứng dụng xác thực (chẳng hạn như Authy hoặc Google Authenticator) để tạo các mã xác nhận theo thời gian thực.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Khóa bảo mật YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Hoạt động với thiết bị YubiKey 4, 4 Nano, 4C và NEO." }, - "duoDesc": { - "message": "Xác minh với Duo Security bằng ứng dụng Duo Mobile, SMS, cuộc gọi điện thoại, hoặc khoá bảo mật U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Mã xác thực sẽ được gửi qua email cho bạn." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "Môi trường tự lưu trữ" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "Passkey" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json index 0706fb85f14..8bb4ad8e6f4 100644 --- a/apps/browser/src/_locales/zh_CN/messages.json +++ b/apps/browser/src/_locales/zh_CN/messages.json @@ -20,7 +20,7 @@ "message": "设置强密码" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "设置密码后就能完成账户创建" + "message": "设置密码以完成账户的创建" }, "login": { "message": "登录" @@ -50,7 +50,7 @@ "message": "主密码提示可以在您忘记密码时帮您回忆起来。" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "如果您忘记了密码,可以发送密码提示到您的电子邮箱。$CURRENT$ / 最多 $MAXIMUM$ 个字符。", "placeholders": { "current": { "content": "$1", @@ -1013,7 +1013,7 @@ "message": "感谢您支持 Bitwarden。" }, "premiumPrice": { - "message": "每年只需 $PRICE$ !", + "message": "只需 $PRICE$ /年!", "placeholders": { "price": { "content": "$1", @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "验证器 App" }, - "authenticatorAppDesc": { - "message": "使用验证器 App(例如 Authy 或 Google Authenticator)来生成基于时间的验证码。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "输入验证器 App(例如 Bitwarden 验证器)生成的代码。", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 安全钥匙" + "yubiKeyTitleV2": { + "message": "Yubico OTP 安全钥匙" }, "yubiKeyDesc": { "message": "使用 YubiKey 来访问您的账户。支持 YubiKey 4、4 Nano、4C 以及 NEO 设备。" }, - "duoDesc": { - "message": "使用 Duo Security 的 Duo 移动应用、短信、电话或 U2F 安全钥匙来进行验证。", + "duoDescV2": { + "message": "输入由 Duo Security 生成的代码。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "电子邮件" }, - "emailDesc": { - "message": "验证码将会发送到您的电子邮箱。" + "emailDescV2": { + "message": "输入发送到您的电子邮箱的代码。" }, "selfHostedEnvironment": { "message": "自托管环境" @@ -2890,7 +2890,7 @@ "description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item." }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切换侧边导航" }, "skipToContent": { "message": "跳转到正文" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "确认文件密码" }, + "exportSuccess": { + "message": "密码库数据已导出" + }, "typePasskey": { "message": "通行密钥" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "无文件夹的项目" }, + "itemDetails": { + "message": "项目详情" + }, + "itemName": { + "message": "项目名称" + }, + "cannotRemoveViewOnlyCollections": { + "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "组织已停用" }, + "owner": { + "message": "所有者" + }, + "selfOwnershipLabel": { + "message": "您", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "无法访问已停用组织中的项目。请联系您的组织所有者获取协助。" }, + "upload": { + "message": "上传" + }, + "addAttachment": { + "message": "添加附件" + }, + "maxFileSizeSansPunctuation": { + "message": "最大文件大小为 500 MB" + }, + "deleteAttachmentName": { + "message": "删除附件 $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "下载 $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "您确定要永久删除此附件吗?" + }, + "premium": { + "message": "高级会员" + }, + "freeOrgsCannotUseAttachments": { + "message": "免费组织无法使用附件" + }, "filters": { - "message": "Filters" + "message": "筛选" + }, + "cardDetails": { + "message": "支付卡详情" + }, + "cardBrandDetails": { + "message": "$BRAND$ 详情", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json index 749e5fe7c25..446439ba2b6 100644 --- a/apps/browser/src/_locales/zh_TW/messages.json +++ b/apps/browser/src/_locales/zh_TW/messages.json @@ -1105,18 +1105,18 @@ "authenticatorAppTitle": { "message": "驗證器應用程式" }, - "authenticatorAppDesc": { - "message": "使用驗證器應用程式 (如 Authy 或 Google Authenticator) 產生基於時間的驗證碼。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 安全鑰匙" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "yubiKeyDesc": { "message": "使用 YubiKey 存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, - "duoDesc": { - "message": "使用 Duo Security 的 Duo Mobile 程式、SMS 、撥打電話或 U2F 安全鑰匙進行驗證。", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -1132,8 +1132,8 @@ "emailTitle": { "message": "電子郵件" }, - "emailDesc": { - "message": "使用電子郵件傳送驗證碼給您。" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "selfHostedEnvironment": { "message": "自我裝載環境" @@ -3107,6 +3107,9 @@ "confirmFilePassword": { "message": "確認檔案密碼" }, + "exportSuccess": { + "message": "Vault data exported" + }, "typePasskey": { "message": "密碼金鑰" }, @@ -3489,13 +3492,83 @@ "itemsWithNoFolder": { "message": "Items with no folder" }, + "itemDetails": { + "message": "Item details" + }, + "itemName": { + "message": "Item name" + }, + "cannotRemoveViewOnlyCollections": { + "message": "You cannot remove collections with View only permissions: $COLLECTIONS$", + "placeholders": { + "collections": { + "content": "$1", + "example": "Work, Personal" + } + } + }, "organizationIsDeactivated": { "message": "Organization is deactivated" }, + "owner": { + "message": "Owner" + }, + "selfOwnershipLabel": { + "message": "You", + "description": "Used as a label to indicate that the user is the owner of an item." + }, "contactYourOrgAdmin": { "message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance." }, + "upload": { + "message": "Upload" + }, + "addAttachment": { + "message": "Add attachment" + }, + "maxFileSizeSansPunctuation": { + "message": "Maximum file size is 500 MB" + }, + "deleteAttachmentName": { + "message": "Delete attachment $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "downloadAttachmentName": { + "message": "Download $NAME$", + "placeholders": { + "name": { + "content": "$1", + "example": "Attachment Name" + } + } + }, + "permanentlyDeleteAttachmentConfirmation": { + "message": "Are you sure you want to permanently delete this attachment?" + }, + "premium": { + "message": "Premium" + }, + "freeOrgsCannotUseAttachments": { + "message": "Free organizations cannot use attachments" + }, "filters": { "message": "Filters" + }, + "cardDetails": { + "message": "Card details" + }, + "cardBrandDetails": { + "message": "$BRAND$ details", + "placeholders": { + "brand": { + "content": "$1", + "example": "Visa" + } + } } } diff --git a/apps/browser/src/auth/popup/home.component.html b/apps/browser/src/auth/popup/home.component.html index 35371948de9..ed395797961 100644 --- a/apps/browser/src/auth/popup/home.component.html +++ b/apps/browser/src/auth/popup/home.component.html @@ -30,7 +30,7 @@ diff --git a/apps/browser/src/auth/popup/home.component.ts b/apps/browser/src/auth/popup/home.component.ts index e647dfd05b9..43f8f3dcf4c 100644 --- a/apps/browser/src/auth/popup/home.component.ts +++ b/apps/browser/src/auth/popup/home.component.ts @@ -1,13 +1,10 @@ import { Component, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject, firstValueFrom, takeUntil } from "rxjs"; +import { Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; import { EnvironmentSelectorComponent } from "@bitwarden/angular/auth/components/environment-selector.component"; -import { LoginEmailServiceAbstraction } from "@bitwarden/auth/common"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; -import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; +import { LoginEmailServiceAbstraction, RegisterRouteService } from "@bitwarden/auth/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -29,29 +26,19 @@ export class HomeComponent implements OnInit, OnDestroy { }); // TODO: remove when email verification flag is removed - registerRoute = "/register"; + registerRoute$ = this.registerRouteService.registerRoute$(); constructor( protected platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, private router: Router, private i18nService: I18nService, - private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, private accountSwitcherService: AccountSwitcherService, - private configService: ConfigService, + private registerRouteService: RegisterRouteService, ) {} async ngOnInit(): Promise { - // TODO: remove when email verification flag is removed - const emailVerification = await this.configService.getFeatureFlag( - FeatureFlag.EmailVerification, - ); - - if (emailVerification) { - this.registerRoute = "/signup"; - } - const email = this.loginEmailService.getEmail(); const rememberEmail = this.loginEmailService.getRememberEmail(); @@ -66,13 +53,14 @@ export class HomeComponent implements OnInit, OnDestroy { } this.environmentSelector.onOpenSelfHostedSettings - .pipe(takeUntil(this.destroyed$)) - .subscribe(() => { - this.setLoginEmailValues(); - // 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(["environment"]); - }); + .pipe( + switchMap(async () => { + await this.setLoginEmailValues(); + await this.router.navigate(["environment"]); + }), + takeUntil(this.destroyed$), + ) + .subscribe(); } ngOnDestroy(): void { @@ -96,12 +84,14 @@ export class HomeComponent implements OnInit, OnDestroy { return; } - this.setLoginEmailValues(); + await this.setLoginEmailValues(); await this.router.navigate(["login"], { queryParams: { email: this.formGroup.value.email } }); } - setLoginEmailValues() { - this.loginEmailService.setEmail(this.formGroup.value.email); + async setLoginEmailValues() { + // Note: Browser saves email settings here instead of the login component this.loginEmailService.setRememberEmail(this.formGroup.value.rememberEmail); + this.loginEmailService.setEmail(this.formGroup.value.email); + await this.loginEmailService.saveEmailSettings(); } } 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 69d3204701e..f83062e6c97 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 @@ -21,8 +21,8 @@ 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ selector: "app-login-via-auth-request", diff --git a/apps/browser/src/auth/popup/login.component.html b/apps/browser/src/auth/popup/login.component.html index 7a4211a5cc1..9d2b4fccad7 100644 --- a/apps/browser/src/auth/popup/login.component.html +++ b/apps/browser/src/auth/popup/login.component.html @@ -52,7 +52,7 @@ diff --git a/apps/browser/src/auth/popup/login.component.ts b/apps/browser/src/auth/popup/login.component.ts index 56a9aca68c7..79a02ede85d 100644 --- a/apps/browser/src/auth/popup/login.component.ts +++ b/apps/browser/src/auth/popup/login.component.ts @@ -8,12 +8,12 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -21,8 +21,8 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../platform/flags"; @@ -52,7 +52,7 @@ export class LoginComponent extends BaseLoginComponent { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, ) { super( devicesApiService, @@ -73,7 +73,7 @@ export class LoginComponent extends BaseLoginComponent { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, ); super.onSuccessfulLogin = async () => { await syncService.fullSync(true); @@ -99,7 +99,7 @@ export class LoginComponent extends BaseLoginComponent { async launchSsoBrowser() { // Save off email for SSO await this.ssoLoginService.setSsoEmail(this.formGroup.value.email); - await this.loginEmailService.saveEmailSettings(); + // Generate necessary sso params const passwordOptions: any = { type: "password", @@ -142,4 +142,9 @@ export class LoginComponent extends BaseLoginComponent { encodeURIComponent(this.formGroup.controls.email.value), ); } + + async saveEmailSettings() { + // values should be saved on home component + return; + } } diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts index f46289f978e..61e007ac52a 100644 --- a/apps/browser/src/auth/popup/register.component.ts +++ b/apps/browser/src/auth/popup/register.component.ts @@ -13,8 +13,8 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ selector: "app-register", diff --git a/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts index 75c3c9e9e0e..c56e6578a0b 100644 --- a/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts +++ b/apps/browser/src/auth/popup/settings/vault-timeout-input.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/angular/components/settings/vault-timeout-input.component"; +import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/auth/angular"; @Component({ selector: "app-vault-timeout-input", diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 14df0d17521..33284717ab5 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -21,8 +21,8 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { BrowserApi } from "../../platform/browser/browser-api"; diff --git a/apps/browser/src/auth/popup/two-factor-auth.component.ts b/apps/browser/src/auth/popup/two-factor-auth.component.ts new file mode 100644 index 00000000000..67ff0fd2857 --- /dev/null +++ b/apps/browser/src/auth/popup/two-factor-auth.component.ts @@ -0,0 +1,153 @@ +import { CommonModule } from "@angular/common"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule } from "@angular/forms"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; + +import { TwoFactorAuthAuthenticatorComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthYubikeyComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth.component"; +import { TwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-options.component"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { SyncService } from "@bitwarden/common/platform/sync"; +import { + ButtonModule, + FormFieldModule, + AsyncActionsModule, + CheckboxModule, + DialogModule, + LinkModule, + TypographyModule, + DialogService, +} from "@bitwarden/components"; + +import { + LoginStrategyServiceAbstraction, + LoginEmailServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "../../../../../libs/auth/src/common/abstractions"; +import { BrowserApi } from "../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../platform/popup/browser-popup-utils"; + +@Component({ + standalone: true, + templateUrl: + "../../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", + selector: "app-two-factor-auth", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + RouterLink, + CheckboxModule, + TwoFactorOptionsComponent, + TwoFactorAuthAuthenticatorComponent, + TwoFactorAuthYubikeyComponent, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent implements OnInit { + constructor( + protected loginStrategyService: LoginStrategyServiceAbstraction, + protected router: Router, + i18nService: I18nService, + platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, + dialogService: DialogService, + protected route: ActivatedRoute, + logService: LogService, + protected twoFactorService: TwoFactorService, + loginEmailService: LoginEmailServiceAbstraction, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + protected ssoLoginService: SsoLoginServiceAbstraction, + protected configService: ConfigService, + masterPasswordService: InternalMasterPasswordServiceAbstraction, + accountService: AccountService, + formBuilder: FormBuilder, + @Inject(WINDOW) protected win: Window, + private syncService: SyncService, + private messagingService: MessagingService, + ) { + super( + loginStrategyService, + router, + i18nService, + platformUtilsService, + environmentService, + dialogService, + route, + logService, + twoFactorService, + loginEmailService, + userDecryptionOptionsService, + ssoLoginService, + configService, + masterPasswordService, + accountService, + formBuilder, + win, + ); + super.onSuccessfulLoginTdeNavigate = async () => { + this.win.close(); + }; + this.onSuccessfulLoginNavigate = this.goAfterLogIn; + } + + async ngOnInit(): Promise { + await super.ngOnInit(); + + if (this.route.snapshot.paramMap.has("webAuthnResponse")) { + // WebAuthn fallback response + this.selectedProviderType = TwoFactorProviderType.WebAuthn; + this.token = this.route.snapshot.paramMap.get("webAuthnResponse"); + super.onSuccessfulLogin = 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.syncService.fullSync(true); + this.messagingService.send("reloadPopup"); + window.close(); + }; + this.remember = this.route.snapshot.paramMap.get("remember") === "true"; + await this.submit(); + return; + } + + if (await BrowserPopupUtils.inPopout(this.win)) { + this.selectedProviderType = TwoFactorProviderType.Email; + } + + // WebAuthn prompt appears inside the popup on linux, and requires a larger popup width + // than usual to avoid cutting off the dialog. + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.add("linux-webauthn"); + } + } + + async ngOnDestroy() { + if (this.selectedProviderType === TwoFactorProviderType.WebAuthn && (await this.isLinux())) { + document.body.classList.remove("linux-webauthn"); + } + } + + async isLinux() { + return (await BrowserApi.getPlatformInfo()).os === "linux"; + } +} diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 5598c27dd6e..9c2e63c1aa7 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -17,7 +17,6 @@ import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { FormData } from "../services/abstractions/autofill.service"; import AutofillService from "../services/autofill.service"; @@ -49,7 +48,6 @@ describe("NotificationBackground", () => { const authService = mock(); const policyService = mock(); const folderService = mock(); - const stateService = mock(); const userNotificationSettingsService = mock(); const domainSettingsService = mock(); const environmentService = mock(); @@ -64,7 +62,6 @@ describe("NotificationBackground", () => { authService, policyService, folderService, - stateService, userNotificationSettingsService, domainSettingsService, environmentService, diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 9b65e4db0b2..179598a8823 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -23,7 +23,6 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; import { openUnlockPopout } from "../../auth/popup/utils/auth-popout-window"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; import { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window"; import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum"; import { AutofillService } from "../services/abstractions/autofill.service"; @@ -76,7 +75,6 @@ export default class NotificationBackground { private authService: AuthService, private policyService: PolicyService, private folderService: FolderService, - private stateService: BrowserStateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private domainSettingsService: DomainSettingsService, private environmentService: EnvironmentService, diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index df4867640f4..7be93b11e6b 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -33,7 +33,6 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherService } from "@bitwarden/common/vault/services/cipher.service"; import { BrowserApi } from "../../platform/browser/browser-api"; -import { DefaultBrowserStateService } from "../../platform/services/default-browser-state.service"; import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service"; import { AutofillService } from "../services/abstractions/autofill.service"; import { @@ -73,7 +72,6 @@ describe("OverlayBackground", () => { urls: { icons: "https://icons.bitwarden.com/" }, }), ); - const stateService = mock(); const autofillSettingsService = mock(); const i18nService = mock(); const platformUtilsService = mock(); @@ -104,7 +102,6 @@ describe("OverlayBackground", () => { authService, environmentService, domainSettingsService, - stateService, autofillSettingsService, i18nService, platformUtilsService, diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index bf954c3419f..2f80790134e 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -9,7 +9,6 @@ import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; 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"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -101,7 +100,6 @@ class OverlayBackground implements OverlayBackgroundInterface { private authService: AuthService, private environmentService: EnvironmentService, private domainSettingsService: DomainSettingsService, - private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts index 67637da2fdd..21eadfaf668 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.spec.ts @@ -6,16 +6,15 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; - import { MainContextMenuHandler } from "./main-context-menu-handler"; describe("context-menu", () => { - let stateService: MockProxy; + let stateService: MockProxy; let autofillSettingsService: MockProxy; let i18nService: MockProxy; let logService: MockProxy; diff --git a/apps/browser/src/autofill/browser/main-context-menu-handler.ts b/apps/browser/src/autofill/browser/main-context-menu-handler.ts index a02a3a84d4b..7b074a566a2 100644 --- a/apps/browser/src/autofill/browser/main-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/main-context-menu-handler.ts @@ -20,12 +20,11 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BrowserStateService } from "../../platform/services/abstractions/browser-state.service"; - import { InitContextMenuItems } from "./abstractions/main-context-menu-handler"; export class MainContextMenuHandler { @@ -143,7 +142,7 @@ export class MainContextMenuHandler { ]; constructor( - private stateService: BrowserStateService, + private stateService: StateService, private autofillSettingsService: AutofillSettingsServiceAbstraction, private i18nService: I18nService, private logService: LogService, diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts index e3639ef81fb..522da229244 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.spec.ts @@ -1,7 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { setAlarmTime } from "../../platform/alarms/alarm-state"; import { BrowserApi } from "../../platform/browser/browser-api"; diff --git a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts index c46718ebad2..dadd61fbd12 100644 --- a/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts +++ b/apps/browser/src/autofill/clipboard/generate-password-to-clipboard-command.ts @@ -1,7 +1,7 @@ import { firstValueFrom } from "rxjs"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { setAlarmTime } from "../../platform/alarms/alarm-state"; diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index 2942ba545ea..0f95cd527ee 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -34,10 +34,23 @@ describe("InlineMenuFieldQualificationService", () => { ); }); + it("has a keyword value that indicates the field is for a create account form", () => { + const field = mock({ + type: "password", + placeholder: "create account password", + autoCompleteType: "", + }); + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + false, + ); + }); + it("has a type that is an excluded type", () => { AutoFillConstants.ExcludedAutofillLoginTypes.forEach((excludedType) => { const field = mock({ type: excludedType, + autoCompleteType: "", }); expect( @@ -53,6 +66,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: index === 0 ? attribute : "", htmlName: index === 1 ? attribute : "", placeholder: index > 1 ? attribute : "", + autoCompleteType: "", }); expect( @@ -67,6 +81,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "not-password", htmlName: "not-password", placeholder: "not-password", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -80,6 +95,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "something-else", htmlName: "something-else", placeholder: "something-else", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -93,6 +109,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "search", htmlName: "something-else", placeholder: "something-else", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -112,12 +129,14 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "", + autoCompleteType: "", }); const secondField = mock({ type: "password", htmlID: "some-other-password", htmlName: "some-other-password", placeholder: "some-other-password", + autoCompleteType: "", }); pageDetails.fields = [field, secondField]; @@ -133,18 +152,21 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "", + autoCompleteType: "", }); const usernameField = mock({ type: "text", htmlID: "user-username", htmlName: "user-username", placeholder: "user-username", + autoCompleteType: "", }); const secondUsernameField = mock({ type: "text", htmlID: "some-other-user-username", htmlName: "some-other-user-username", placeholder: "some-other-user-username", + autoCompleteType: "", }); pageDetails.fields = [field, usernameField, secondUsernameField]; @@ -186,6 +208,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "validFormId", + autoCompleteType: "", }); const secondField = mock({ type: "password", @@ -193,6 +216,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "validFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondField]; @@ -218,12 +242,35 @@ describe("InlineMenuFieldQualificationService", () => { ); }); + it("is structured on a page with a single set of username and password fields", () => { + const field = mock({ + type: "password", + htmlID: "user-password", + htmlName: "user-password", + placeholder: "user-password", + autoCompleteType: "", + }); + const usernameField = mock({ + type: "text", + htmlID: "user-username", + htmlName: "user-username", + placeholder: "user-username", + autoCompleteType: "", + }); + pageDetails.fields = [field, usernameField]; + + expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( + true, + ); + }); + it("has a type of `text` with an attribute that indicates the field is a password field", () => { const field = mock({ type: "text", htmlID: null, htmlName: "user-password", placeholder: "user-password", + autoCompleteType: "", }); expect(inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails)).toBe( @@ -247,6 +294,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlID: "user-username", htmlName: "user-username", placeholder: "user-username", + autoCompleteType: "", }); pageDetails.fields = [field, usernameField]; @@ -273,6 +321,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-password", placeholder: "user-password", form: "validFormId", + autoCompleteType: "", }); const secondPasswordField = mock({ type: "password", @@ -280,6 +329,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "anotherFormId", + autoCompleteType: "", }); const usernameField = mock({ type: "text", @@ -287,6 +337,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "user-username", placeholder: "user-username", form: "validFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondPasswordField, usernameField]; @@ -310,6 +361,7 @@ describe("InlineMenuFieldQualificationService", () => { htmlName: "some-other-password", placeholder: "some-other-password", form: "anotherFormId", + autoCompleteType: "", }); pageDetails.fields = [field, secondPasswordField]; @@ -347,21 +399,23 @@ describe("InlineMenuFieldQualificationService", () => { }); }); - ["new", "change", "neue", "ändern"].forEach((keyword) => { - it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => { - const field = mock({ - type: "text", - autoCompleteType: "", - htmlID: "user-username", - htmlName: "user-username", - placeholder: `${keyword} username`, - }); + ["new", "change", "neue", "ändern", "register", "create", "registration"].forEach( + (keyword) => { + it(`has a keyword of ${keyword} that indicates a 'new or changed' username is being filled`, () => { + const field = mock({ + type: "text", + autoCompleteType: "", + htmlID: "user-username", + htmlName: "user-username", + placeholder: `${keyword} username`, + }); - expect( - inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), - ).toBe(false); - }); - }); + expect( + inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), + ).toBe(false); + }); + }, + ); describe("does not have a parent form element", () => { beforeEach(() => { diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 582f8889daa..7bc027b392c 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -14,9 +14,18 @@ export class InlineMenuFieldQualificationService private usernameAutocompleteValues = new Set(["username", "email"]); private fieldIgnoreListString = AutoFillConstants.FieldIgnoreList.join(","); private passwordFieldExcludeListString = AutoFillConstants.PasswordFieldExcludeList.join(","); + private currentPasswordAutocompleteValues = new Set(["current-password"]); + private newPasswordAutoCompleteValues = new Set(["new-password"]); private autofillFieldKeywordsMap: WeakMap = new WeakMap(); private autocompleteDisabledValues = new Set(["off", "false"]); private newFieldKeywords = new Set(["new", "change", "neue", "ändern"]); + private accountCreationFieldKeywords = new Set([ + "register", + "registration", + "create", + "confirm", + ...this.newFieldKeywords, + ]); private inlineMenuFieldQualificationFlagSet = false; constructor() { @@ -62,7 +71,12 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete value of "current-password", we should assume that // the page developer intends for this field to be interpreted as a password field for a login form. - if (field.autoCompleteType === "current-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.currentPasswordAutocompleteValues, + ) + ) { return true; } @@ -95,7 +109,11 @@ export class InlineMenuFieldQualificationService // If a single username field or less is present on the page, then we can assume that the // provided field is for a login form. This will only be the case if the field does not // explicitly have its autocomplete attribute set to "off" or "false". - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } // If the field has a form parent and there are multiple visible password fields @@ -117,7 +135,10 @@ export class InlineMenuFieldQualificationService // If the field has a form parent and no username field exists and the field has an // autocomplete attribute set to "off" or "false", this is not a password field - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } /** @@ -132,7 +153,9 @@ export class InlineMenuFieldQualificationService ): boolean { // If the provided field is set with an autocomplete of "username", we should assume that // the page developer intends for this field to be interpreted as a username field. - if (this.usernameAutocompleteValues.has(field.autoCompleteType)) { + if ( + this.fieldContainsAutocompleteValues(field.autoCompleteType, this.usernameAutocompleteValues) + ) { const newPasswordFieldsInPageDetails = pageDetails.fields.filter(this.isNewPasswordField); return newPasswordFieldsInPageDetails.length === 0; } @@ -175,7 +198,10 @@ export class InlineMenuFieldQualificationService // If the page does not contain any password fields, it might be part of a multistep login form. // That will only be the case if the field does not explicitly have its autocomplete attribute // set to "off" or "false". - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } // If the field is structured within a form, but no password fields are present in the form, @@ -183,7 +209,12 @@ export class InlineMenuFieldQualificationService if (passwordFieldsInPageDetails.length === 0) { // If the field's autocomplete is set to a disabled value, we should assume that the field is // not part of a login form. - if (this.autocompleteDisabledValues.has(field.autoCompleteType)) { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ) + ) { return false; } @@ -212,7 +243,10 @@ export class InlineMenuFieldQualificationService // If no visible password fields are found, this field might be part of a multipart form. // Check for an invalid autocompleteType to determine if the field is part of a login form. - return !this.autocompleteDisabledValues.has(field.autoCompleteType); + return !this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.autocompleteDisabledValues, + ); } /** @@ -237,7 +271,13 @@ export class InlineMenuFieldQualificationService * @param field - The field to validate */ private isCurrentPasswordField = (field: AutofillField): boolean => { - if (field.autoCompleteType === "new-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.newPasswordAutoCompleteValues, + ) || + this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + ) { return false; } @@ -250,11 +290,19 @@ export class InlineMenuFieldQualificationService * @param field - The field to validate */ private isNewPasswordField = (field: AutofillField): boolean => { - if (field.autoCompleteType === "current-password") { + if ( + this.fieldContainsAutocompleteValues( + field.autoCompleteType, + this.currentPasswordAutocompleteValues, + ) + ) { return false; } - return this.isPasswordField(field); + return ( + this.isPasswordField(field) && + this.keywordsFoundInFieldData(field, [...this.accountCreationFieldKeywords]) + ); }; /** @@ -422,6 +470,31 @@ export class InlineMenuFieldQualificationService return keywordValues; } + /** + * Separates the provided field data into space-separated values and checks if any + * of the values are present in the provided set of autocomplete values. + * + * @param fieldAutocompleteValue - The field autocomplete value to validate + * @param compareValues - The set of autocomplete values to check against + */ + private fieldContainsAutocompleteValues( + fieldAutocompleteValue: string, + compareValues: Set, + ) { + if (!fieldAutocompleteValue) { + return false; + } + + const autocompleteValueParts = fieldAutocompleteValue.split(" "); + for (let index = 0; index < autocompleteValueParts.length; index++) { + if (compareValues.has(autocompleteValueParts[index])) { + return true; + } + } + + return false; + } + /** * This method represents the previous rudimentary approach to qualifying fields for login forms. * diff --git a/apps/browser/src/background/commands.background.ts b/apps/browser/src/background/commands.background.ts index 10395e66e5a..f2152c7f9f4 100644 --- a/apps/browser/src/background/commands.background.ts +++ b/apps/browser/src/background/commands.background.ts @@ -2,7 +2,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { openUnlockPopout } from "../auth/popup/utils/auth-popout-window"; import { LockedVaultPendingNotificationsData } from "../autofill/background/abstractions/notification.background"; diff --git a/apps/browser/src/background/idle.background.ts b/apps/browser/src/background/idle.background.ts index eef033b364b..c0cd3a86aae 100644 --- a/apps/browser/src/background/idle.background.ts +++ b/apps/browser/src/background/idle.background.ts @@ -7,8 +7,6 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; - const IdleInterval = 60 * 5; // 5 minutes export default class IdleBackground { @@ -18,7 +16,6 @@ export default class IdleBackground { constructor( private vaultTimeoutService: VaultTimeoutService, - private stateService: BrowserStateService, private notificationsService: NotificationsService, private accountService: AccountService, private vaultTimeoutSettingsService: VaultTimeoutSettingsService, diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 8e11e7e193e..d438bced4b3 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -8,7 +8,6 @@ import { AuthRequestServiceAbstraction, AuthRequestService, LoginEmailServiceAbstraction, - LoginEmailService, LogoutReason, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; @@ -87,6 +86,7 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService as LogServiceAbstraction } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; import { AbstractStorageService, ObservableStorageService, @@ -102,6 +102,7 @@ import { Message, MessageListener, MessageSender } from "@bitwarden/common/platf import { SubjectMessageSender } from "@bitwarden/common/platform/messaging/internal"; import { Lazy } from "@bitwarden/common/platform/misc/lazy"; import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; +import { Account } from "@bitwarden/common/platform/models/domain/account"; 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"; @@ -117,6 +118,7 @@ import { FileUploadService } from "@bitwarden/common/platform/services/file-uplo import { KeyGenerationService } from "@bitwarden/common/platform/services/key-generation.service"; import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service"; import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; +import { StateService } from "@bitwarden/common/platform/services/state.service"; 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"; @@ -147,12 +149,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 { - legacyPasswordGenerationServiceFactory, - legacyUsernameGenerationServiceFactory, -} from "@bitwarden/common/tools/generator"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, @@ -179,6 +175,12 @@ import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service"; +import { + legacyPasswordGenerationServiceFactory, + PasswordGenerationServiceAbstraction, + legacyUsernameGenerationServiceFactory, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; import { ImportApiService, ImportApiServiceAbstraction, @@ -207,22 +209,18 @@ import { Fido2Background } from "../autofill/fido2/background/fido2.background"; import { AutofillService as AutofillServiceAbstraction } from "../autofill/services/abstractions/autofill.service"; import AutofillService from "../autofill/services/autofill.service"; import { SafariApp } from "../browser/safariApp"; -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 { 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"; import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service"; @@ -267,8 +265,8 @@ export default class MainBackground { collectionService: CollectionServiceAbstraction; vaultTimeoutService: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; - syncService: SyncService; passwordGenerationService: PasswordGenerationServiceAbstraction; + syncService: SyncService; passwordStrengthService: PasswordStrengthServiceAbstraction; totpService: TotpServiceAbstraction; autofillService: AutofillServiceAbstraction; @@ -485,14 +483,13 @@ 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 = BrowserApi.isManifestVersion(2) + ? new MultithreadEncryptServiceImplementation( + this.cryptoFunctionService, + this.logService, + true, + ) + : new EncryptServiceImplementation(this.cryptoFunctionService, this.logService, true); this.singleUserStateProvider = new DefaultSingleUserStateProvider( storageServiceProvider, @@ -541,7 +538,7 @@ export default class MainBackground { ClientType.Browser, ); - this.stateService = new DefaultBrowserStateService( + this.stateService = new StateService( this.storageService, this.secureStorageService, this.memoryStorageService, @@ -708,8 +705,6 @@ export default class MainBackground { this.stateProvider, ); - this.loginEmailService = new LoginEmailService(this.stateProvider); - this.ssoLoginService = new SsoLoginService(this.stateProvider); this.userVerificationApiService = new UserVerificationApiService(this.apiService); @@ -869,6 +864,7 @@ export default class MainBackground { this.organizationService, this.eventUploadService, this.authService, + this.accountService, ); this.totpService = new TotpService(this.cryptoFunctionService, this.logService); @@ -970,7 +966,6 @@ export default class MainBackground { this.messagingService, this.platformUtilsService, systemUtilsServiceReloadCallback, - this.stateService, this.autofillSettingsService, this.vaultTimeoutSettingsService, this.biometricStateService, @@ -1030,7 +1025,6 @@ export default class MainBackground { this.authService, this.policyService, this.folderService, - this.stateService, this.userNotificationSettingsService, this.domainSettingsService, this.environmentService, @@ -1044,7 +1038,6 @@ export default class MainBackground { this.authService, this.environmentService, this.domainSettingsService, - this.stateService, this.autofillSettingsService, this.i18nService, this.platformUtilsService, @@ -1102,7 +1095,6 @@ export default class MainBackground { this.idleBackground = new IdleBackground( this.vaultTimeoutService, - this.stateService, this.notificationsService, this.accountService, this.vaultTimeoutSettingsService, @@ -1229,11 +1221,6 @@ export default class MainBackground { async switchAccount(userId: UserId) { let nextAccountStatus: AuthenticationStatus; try { - 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); // HACK to ensure account is switched before proceeding const switchPromise = firstValueFrom( this.accountService.activeAccount$.pipe( @@ -1254,9 +1241,6 @@ export default class MainBackground { clearCaches(); if (userId == null) { - this.loginEmailService.setRememberEmail(false); - await this.loginEmailService.saveEmailSettings(); - await this.refreshBadge(); await this.refreshMenu(); await this.overlayBackground?.updateOverlayCiphers(); // null in popup only contexts diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json index 55b3b8f84cd..b1c51911ec8 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.6.3", + "version": "2024.7.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json index ade0ebaf2bc..40060a7fd93 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.6.3", + "version": "2024.7.0", "description": "__MSG_extDesc__", "default_locale": "en", "author": "Bitwarden Inc.", diff --git a/apps/browser/src/models/account.ts b/apps/browser/src/models/account.ts deleted file mode 100644 index 519f1bda6bb..00000000000 --- a/apps/browser/src/models/account.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { Account as BaseAccount } from "@bitwarden/common/platform/models/domain/account"; - -import { BrowserComponentState } from "./browserComponentState"; -import { BrowserGroupingsComponentState } from "./browserGroupingsComponentState"; -import { BrowserSendComponentState } from "./browserSendComponentState"; - -export class Account extends BaseAccount { - groupings?: BrowserGroupingsComponentState; - send?: BrowserSendComponentState; - ciphers?: BrowserComponentState; - sendType?: BrowserComponentState; - - constructor(init: Partial) { - super(init); - - this.groupings = init?.groupings ?? new BrowserGroupingsComponentState(); - this.send = init?.send ?? new BrowserSendComponentState(); - this.ciphers = init?.ciphers ?? new BrowserComponentState(); - this.sendType = init?.sendType ?? new BrowserComponentState(); - } - - static fromJSON(json: Jsonify): Account { - if (json == null) { - return null; - } - - return Object.assign(new Account({}), json, super.fromJSON(json), { - groupings: BrowserGroupingsComponentState.fromJSON(json.groupings), - send: BrowserSendComponentState.fromJSON(json.send), - ciphers: BrowserComponentState.fromJSON(json.ciphers), - sendType: BrowserComponentState.fromJSON(json.sendType), - }); - } -} diff --git a/apps/browser/src/platform/popup/layout/popup-footer.component.html b/apps/browser/src/platform/popup/layout/popup-footer.component.html index 2cbbca79c0b..777e0ab60da 100644 --- a/apps/browser/src/platform/popup/layout/popup-footer.component.html +++ b/apps/browser/src/platform/popup/layout/popup-footer.component.html @@ -1,9 +1,12 @@
-
+
+
+ +
diff --git a/apps/browser/src/platform/popup/layout/popup-header.component.html b/apps/browser/src/platform/popup/layout/popup-header.component.html index 55caf1b91e7..e866ba4e81f 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.html +++ b/apps/browser/src/platform/popup/layout/popup-header.component.html @@ -9,7 +9,7 @@ *ngIf="showBackButton" [title]="'back' | i18n" [ariaLabel]="'back' | i18n" - (click)="back()" + [bitAction]="backAction" >

{{ 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 index f2f8eb95af0..1b491ea881c 100644 --- a/apps/browser/src/platform/popup/layout/popup-header.component.ts +++ b/apps/browser/src/platform/popup/layout/popup-header.component.ts @@ -3,13 +3,18 @@ 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"; +import { + AsyncActionsModule, + FunctionReturningAwaitable, + IconButtonModule, + TypographyModule, +} from "@bitwarden/components"; @Component({ selector: "popup-header", templateUrl: "popup-header.component.html", standalone: true, - imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule], + imports: [TypographyModule, CommonModule, IconButtonModule, JslibModule, AsyncActionsModule], }) export class PopupHeaderComponent { /** Display the back button, which uses Location.back() to go back one page in history */ @@ -26,9 +31,15 @@ export class PopupHeaderComponent { /** Title string that will be inserted as an h1 */ @Input({ required: true }) pageTitle: string; - constructor(private location: Location) {} - - back() { + /** + * Async action that occurs when clicking the back button + * + * If unset, will call `location.back()` + **/ + @Input() + backAction: FunctionReturningAwaitable = async () => { this.location.back(); - } + }; + + constructor(private location: Location) {} } diff --git a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts index cc7758d9680..9883a5cfb6f 100644 --- a/apps/browser/src/platform/popup/layout/popup-layout.stories.ts +++ b/apps/browser/src/platform/popup/layout/popup-layout.stories.ts @@ -266,6 +266,7 @@ class MockSettingsPageComponent {} + `, @@ -279,6 +280,7 @@ class MockSettingsPageComponent {} MockPopoutButtonComponent, MockCurrentAccountComponent, VaultComponent, + IconButtonModule, ], }) class MockVaultSubpageComponent {} diff --git a/apps/browser/src/platform/services/abstractions/browser-state.service.ts b/apps/browser/src/platform/services/abstractions/browser-state.service.ts deleted file mode 100644 index c8e2c502e71..00000000000 --- a/apps/browser/src/platform/services/abstractions/browser-state.service.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; - -import { Account } from "../../../models/account"; - -export abstract class BrowserStateService extends BaseStateServiceAbstraction {} diff --git a/apps/browser/src/platform/services/browser-state.service.spec.ts b/apps/browser/src/platform/services/browser-state.service.spec.ts deleted file mode 100644 index 506f185b649..00000000000 --- a/apps/browser/src/platform/services/browser-state.service.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { mock, MockProxy } from "jest-mock-extended"; - -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 } 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"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { mockAccountServiceWith } from "@bitwarden/common/spec"; -import { UserId } from "@bitwarden/common/types/guid"; - -import { Account } from "../../models/account"; - -import { DefaultBrowserStateService } from "./default-browser-state.service"; - -describe("Browser State Service", () => { - let secureStorageService: MockProxy; - let diskStorageService: MockProxy; - let logService: MockProxy; - let stateFactory: MockProxy>; - let environmentService: MockProxy; - let tokenService: MockProxy; - let migrationRunner: MockProxy; - - let state: State; - const userId = "userId" as UserId; - const accountService = mockAccountServiceWith(userId); - - let sut: DefaultBrowserStateService; - - beforeEach(() => { - secureStorageService = mock(); - diskStorageService = mock(); - logService = mock(); - stateFactory = mock(); - environmentService = mock(); - tokenService = mock(); - migrationRunner = mock(); - - state = new State(new GlobalState()); - state.accounts[userId] = new Account({ - profile: { userId: userId }, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - describe("state methods", () => { - let memoryStorageService: MockProxy; - - beforeEach(() => { - memoryStorageService = mock(); - const stateGetter = (key: string) => Promise.resolve(state); - memoryStorageService.get.mockImplementation(stateGetter); - - sut = new DefaultBrowserStateService( - diskStorageService, - secureStorageService, - memoryStorageService, - logService, - stateFactory, - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }); - - 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 deleted file mode 100644 index 92da28efa25..00000000000 --- a/apps/browser/src/platform/services/default-browser-state.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -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 } 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"; -import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner"; -import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; - -import { Account } from "../../models/account"; - -import { BrowserStateService } from "./abstractions/browser-state.service"; - -export class DefaultBrowserStateService - extends BaseStateService - implements BrowserStateService -{ - protected accountDeserializer = Account.fromJSON; - - constructor( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractStorageService, - logService: LogService, - stateFactory: StateFactory, - accountService: AccountService, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) { - super( - storageService, - secureStorageService, - memoryStorageService, - logService, - stateFactory, - accountService, - environmentService, - tokenService, - migrationRunner, - ); - } - - async addAccount(account: Account) { - // Apply browser overrides to default account values - account = new Account(account); - await super.addAccount(account); - } - - // 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( - account: Account, - options: StorageOptions, - ): Promise { - const storageLocation = options.useSecureStorage - ? this.secureStorageService - : this.storageService; - - await storageLocation.save(`${options.userId}`, account, options); - } -} diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index 51152ba0f71..8645cb797bd 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -19,6 +19,7 @@ import { } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { fido2AuthGuard } from "../auth/guards/fido2-auth.guard"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { EnvironmentComponent } from "../auth/popup/environment.component"; @@ -33,6 +34,7 @@ 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 { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component"; import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; @@ -68,6 +70,8 @@ import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component"; +import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component"; +import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component"; import { AppearanceComponent } from "../vault/popup/settings/appearance.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component"; @@ -136,12 +140,26 @@ const routes: Routes = [ canActivate: [lockGuard()], data: { state: "lock", doNotSaveUrl: true }, }, - { - path: "2fa", - component: TwoFactorComponent, - canActivate: [unauthGuardFn(unauthRouteOverrides)], - data: { state: "2fa" }, - }, + ...twofactorRefactorSwap( + TwoFactorComponent, + AnonLayoutWrapperComponent, + { + path: "2fa", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { state: "2fa" }, + }, + { + path: "2fa", + canActivate: [unauthGuardFn(unauthRouteOverrides)], + data: { state: "2fa" }, + children: [ + { + path: "", + component: TwoFactorAuthComponent, + }, + ], + }, + ), { path: "2fa-options", component: TwoFactorOptionsComponent, @@ -194,12 +212,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "ciphers" }, }, - { + ...extensionRefreshSwap(ViewComponent, ViewV2Component, { path: "view-cipher", - component: ViewComponent, canActivate: [AuthGuard], data: { state: "view-cipher" }, - }, + }), { path: "cipher-password-history", component: PasswordHistoryComponent, @@ -230,12 +247,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "collections" }, }, - { + ...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, { path: "attachments", - component: AttachmentsComponent, canActivate: [AuthGuard], data: { state: "attachments" }, - }, + }), { path: "generator", component: GeneratorComponent, @@ -323,12 +339,11 @@ const routes: Routes = [ canActivate: [AuthGuard], data: { state: "appearance" }, }, - { + ...extensionRefreshSwap(AddEditComponent, AddEditV2Component, { path: "clone-cipher", - component: AddEditComponent, canActivate: [AuthGuard], data: { state: "clone-cipher" }, - }, + }), { path: "send-type", component: SendTypeComponent, diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index b70a5564ed9..287e9096684 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -8,6 +8,7 @@ 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -19,7 +20,6 @@ import { } from "@bitwarden/components"; import { BrowserApi } from "../platform/browser/browser-api"; -import { BrowserStateService } from "../platform/services/abstractions/browser-state.service"; import { BrowserSendStateService } from "../tools/popup/services/browser-send-state.service"; import { VaultBrowserStateService } from "../vault/services/vault-browser-state.service"; @@ -45,7 +45,7 @@ export class AppComponent implements OnInit, OnDestroy { private authService: AuthService, private i18nService: I18nService, private router: Router, - private stateService: BrowserStateService, + private stateService: StateService, private browserSendStateService: BrowserSendStateService, private vaultBrowserStateService: VaultBrowserStateService, private cipherService: CipherService, diff --git a/apps/browser/src/popup/images/two-factor/0.png b/apps/browser/src/popup/images/two-factor/0.png new file mode 100644 index 00000000000..307ff4fd60f Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/0.png differ diff --git a/apps/browser/src/popup/images/two-factor/1-w.png b/apps/browser/src/popup/images/two-factor/1-w.png new file mode 100644 index 00000000000..a4e39b3f466 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/1-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/1.png b/apps/browser/src/popup/images/two-factor/1.png new file mode 100644 index 00000000000..37fb7bc4327 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/1.png differ diff --git a/apps/browser/src/popup/images/two-factor/2.png b/apps/browser/src/popup/images/two-factor/2.png new file mode 100644 index 00000000000..d069bdab992 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/2.png differ diff --git a/apps/browser/src/popup/images/two-factor/3.png b/apps/browser/src/popup/images/two-factor/3.png new file mode 100644 index 00000000000..c543343f53b Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/3.png differ diff --git a/apps/browser/src/popup/images/two-factor/4.png b/apps/browser/src/popup/images/two-factor/4.png new file mode 100644 index 00000000000..058671ea37e Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/4.png differ diff --git a/apps/browser/src/popup/images/two-factor/6.png b/apps/browser/src/popup/images/two-factor/6.png new file mode 100644 index 00000000000..d069bdab992 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/6.png differ diff --git a/apps/browser/src/popup/images/two-factor/7-w.png b/apps/browser/src/popup/images/two-factor/7-w.png new file mode 100644 index 00000000000..89fdd8a2a08 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/7-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/7.png b/apps/browser/src/popup/images/two-factor/7.png new file mode 100644 index 00000000000..2a38bdcd3e5 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/7.png differ diff --git a/apps/browser/src/popup/images/two-factor/rc-w.png b/apps/browser/src/popup/images/two-factor/rc-w.png new file mode 100644 index 00000000000..e83b8db1324 Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/rc-w.png differ diff --git a/apps/browser/src/popup/images/two-factor/rc.png b/apps/browser/src/popup/images/two-factor/rc.png new file mode 100644 index 00000000000..4bebdf936cc Binary files /dev/null and b/apps/browser/src/popup/images/two-factor/rc.png differ diff --git a/apps/browser/src/popup/scss/plugins.scss b/apps/browser/src/popup/scss/plugins.scss new file mode 100644 index 00000000000..b8ac8697b7f --- /dev/null +++ b/apps/browser/src/popup/scss/plugins.scss @@ -0,0 +1,31 @@ +@import "variables.scss"; + +@each $mfaType in $mfaTypes { + .mfaType#{$mfaType} { + content: url("../images/two-factor/" + $mfaType + ".png"); + max-width: 100px; + } +} + +.mfaType1 { + @include themify($themes) { + content: url("../images/two-factor/1" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} + +.mfaType7 { + @include themify($themes) { + content: url("../images/two-factor/7" + themed("mfaLogoSuffix")); + max-width: 100px; + } +} + +.recovery-code-img { + @include themify($themes) { + content: url("../images/two-factor/rc" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} diff --git a/apps/browser/src/popup/scss/popup.scss b/apps/browser/src/popup/scss/popup.scss index 850ef96c64e..8dc2c206f87 100644 --- a/apps/browser/src/popup/scss/popup.scss +++ b/apps/browser/src/popup/scss/popup.scss @@ -10,5 +10,6 @@ @import "modal.scss"; @import "environment.scss"; @import "pages.scss"; +@import "plugins.scss"; @import "@angular/cdk/overlay-prebuilt.css"; @import "../../../../../libs/components/src/multi-select/scss/bw.theme"; diff --git a/apps/browser/src/popup/scss/variables.scss b/apps/browser/src/popup/scss/variables.scss index db38a044610..9720ab4d2ce 100644 --- a/apps/browser/src/popup/scss/variables.scss +++ b/apps/browser/src/popup/scss/variables.scss @@ -19,6 +19,8 @@ $border-radius: 6px; $line-height-base: 1.42857143; $icon-hover-color: lighten($text-color, 50%); +$mfaTypes: 0, 2, 3, 4, 6; + $gray: #555; $gray-light: #777; $text-muted: $gray-light; @@ -111,6 +113,7 @@ $themes: ( infoColor: $brand-info, warningColor: $brand-warning, logoSuffix: "dark", + mfaLogoSuffix: ".png", passwordNumberColor: #007fde, passwordSpecialColor: #c40800, passwordCountText: #212529, @@ -174,6 +177,7 @@ $themes: ( infoColor: #a4b0c6, warningColor: #ffeb66, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: #6f9df1, passwordSpecialColor: #ff8d85, passwordCountText: #ffffff, @@ -236,6 +240,7 @@ $themes: ( infoColor: $nord9, warningColor: $nord12, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $nord8, passwordSpecialColor: $nord12, passwordCountText: $nord5, @@ -298,6 +303,7 @@ $themes: ( infoColor: $solarizedDarkGreen, warningColor: $solarizedDarkYellow, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $solarizedDarkCyan, passwordSpecialColor: $solarizedDarkYellow, passwordCountText: $solarizedDarkBase2, diff --git a/apps/browser/src/popup/services/init.service.ts b/apps/browser/src/popup/services/init.service.ts index 63ce45c9b76..9e6471eaf28 100644 --- a/apps/browser/src/popup/services/init.service.ts +++ b/apps/browser/src/popup/services/init.service.ts @@ -6,16 +6,16 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor 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"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; 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 stateService: StateService, private twoFactorService: TwoFactorService, private logService: LogServiceAbstraction, private themingService: AbstractThemingService, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index c7b5ca9b416..e82eb429a5a 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -1,6 +1,6 @@ import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core"; import { Router } from "@angular/router"; -import { Subject, merge } from "rxjs"; +import { Subject, merge, of } from "rxjs"; import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards"; import { AngularThemingService } from "@bitwarden/angular/platform/services/theming/angular-theming.service"; @@ -17,7 +17,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common"; +import { PinServiceAbstraction } 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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; @@ -32,7 +32,6 @@ import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config 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 { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AutofillSettingsService, @@ -57,20 +56,17 @@ import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platfor import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService as BaseStateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { 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 { @@ -94,7 +90,6 @@ import { UnauthGuardService } from "../../auth/popup/services"; import { AutofillService as AutofillServiceAbstraction } from "../../autofill/services/abstractions/autofill.service"; 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 */ @@ -104,13 +99,11 @@ import { OffscreenDocumentService } from "../../platform/offscreen-document/abst 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 { 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 { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; @@ -219,7 +212,7 @@ const safeProviders: SafeProvider[] = [ encryptService: EncryptService, platformUtilsService: PlatformUtilsService, logService: LogService, - stateService: StateServiceAbstraction, + stateService: StateService, accountService: AccountServiceAbstraction, stateProvider: StateProvider, biometricStateService: BiometricStateService, @@ -250,7 +243,7 @@ const safeProviders: SafeProvider[] = [ EncryptService, PlatformUtilsService, LogService, - StateServiceAbstraction, + StateService, AccountServiceAbstraction, StateProvider, BiometricStateService, @@ -262,11 +255,6 @@ const safeProviders: SafeProvider[] = [ useClass: TotpService, deps: [CryptoFunctionService, LogService], }), - safeProvider({ - provide: AuthRequestServiceAbstraction, - useFactory: getBgService("authRequestService"), - deps: [], - }), safeProvider({ provide: DeviceTrustServiceAbstraction, useFactory: getBgService("deviceTrustService"), @@ -436,46 +424,6 @@ const safeProviders: SafeProvider[] = [ }, deps: [StateProvider], }), - safeProvider({ - provide: StateServiceAbstraction, - useFactory: ( - storageService: AbstractStorageService, - secureStorageService: AbstractStorageService, - memoryStorageService: AbstractStorageService, - logService: LogService, - accountService: AccountServiceAbstraction, - environmentService: EnvironmentService, - tokenService: TokenService, - migrationRunner: MigrationRunner, - ) => { - return new DefaultBrowserStateService( - storageService, - secureStorageService, - memoryStorageService, - logService, - new StateFactory(GlobalState, Account), - accountService, - environmentService, - tokenService, - migrationRunner, - ); - }, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - ], - }), - safeProvider({ - provide: BaseStateServiceAbstraction, - useExisting: StateServiceAbstraction, - deps: [], - }), safeProvider({ provide: FileDownloadService, useClass: BrowserFileDownloadService, @@ -485,14 +433,15 @@ const safeProviders: SafeProvider[] = [ provide: SYSTEM_THEME_OBSERVABLE, useFactory: (platformUtilsService: PlatformUtilsService) => { // Safari doesn't properly handle the (prefers-color-scheme) media query in the popup window, it always returns light. - // In Safari, we have to use the background page instead, which comes with limitations like not dynamically changing the extension theme when the system theme is changed. - let windowContext = window; + // This means we have to use the background page instead, which comes with limitations like not dynamically + // changing the extension theme when the system theme is changed. We also have issues with memory leaks when + // holding the reference to the background page. const backgroundWindow = BrowserApi.getBackgroundPage(); if (platformUtilsService.isSafari() && backgroundWindow) { - windowContext = backgroundWindow; + return of(AngularThemingService.getSystemThemeFromWindow(backgroundWindow)); + } else { + return AngularThemingService.createSystemThemeFromWindow(window); } - - return AngularThemingService.createSystemThemeFromWindow(windowContext); }, deps: [PlatformUtilsService], }), diff --git a/apps/browser/src/tools/popup/generator/generator.component.ts b/apps/browser/src/tools/popup/generator/generator.component.ts index cb671f7201a..8746456ec0a 100644 --- a/apps/browser/src/tools/popup/generator/generator.component.ts +++ b/apps/browser/src/tools/popup/generator/generator.component.ts @@ -8,12 +8,14 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { AddEditCipherInfo } from "@bitwarden/common/vault/types/add-edit-cipher-info"; import { ToastService } from "@bitwarden/components"; +import { + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; @Component({ selector: "app-generator", diff --git a/apps/browser/src/tools/popup/generator/password-generator-history.component.ts b/apps/browser/src/tools/popup/generator/password-generator-history.component.ts index e27c42a111b..2436ae51a7d 100644 --- a/apps/browser/src/tools/popup/generator/password-generator-history.component.ts +++ b/apps/browser/src/tools/popup/generator/password-generator-history.component.ts @@ -4,8 +4,8 @@ import { Component } from "@angular/core"; import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { ToastService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ selector: "app-password-generator-history", 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 7f172178163..697188e23ac 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 @@ -13,12 +13,12 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendService } from "@bitwarden/common/tools/send/services/send.service.abstraction"; import { DialogService, ToastService } from "@bitwarden/components"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; -import { BrowserStateService } from "../../../platform/services/abstractions/browser-state.service"; import { FilePopoutUtilsService } from "../services/file-popout-utils.service"; @Component({ @@ -37,7 +37,7 @@ export class SendAddEditComponent extends BaseAddEditComponent { constructor( i18nService: I18nService, platformUtilsService: PlatformUtilsService, - stateService: BrowserStateService, + stateService: StateService, messagingService: MessagingService, policyService: PolicyService, environmentService: EnvironmentService, diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html index edf20d3e9c3..852fd4a0e81 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html +++ b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.html @@ -28,6 +28,7 @@ + {{ getSubName(cipher) }} {{ cipher.subTitle }} diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts index c07d2ef8860..25d623b1692 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2-cipher-row.component.ts @@ -1,10 +1,11 @@ -import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Component, EventEmitter, Input, Output, ChangeDetectionStrategy } from "@angular/core"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @Component({ selector: "app-fido2-cipher-row", templateUrl: "fido2-cipher-row.component.html", + changeDetection: ChangeDetectionStrategy.OnPush, }) export class Fido2CipherRowComponent { @Output() onSelected = new EventEmitter(); @@ -17,4 +18,22 @@ export class Fido2CipherRowComponent { protected selectCipher(c: CipherView) { this.onSelected.emit(c); } + + /** + * Returns a subname for the cipher. + * If this has a FIDO2 credential, and the cipher.name is different from the FIDO2 credential's rpId, return the rpId. + * @param c Cipher + * @returns + */ + protected getSubName(c: CipherView): string | null { + const fido2Credentials = c.login?.fido2Credentials; + + if (!fido2Credentials || fido2Credentials.length === 0) { + return null; + } + + const [fido2Credential] = fido2Credentials; + + return c.name !== fido2Credential.rpId ? fido2Credential.rpId : null; + } } diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 049dc30ef61..752a9100721 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -256,7 +256,7 @@ export class Fido2Component implements OnInit, OnDestroy { const name = data.credentialName || data.rpId; // TODO: Revert to check for user verification once user verification for passkeys is approved for production. // PM-4577 - https://github.com/bitwarden/clients/pull/8746 - await this.createNewCipher(name); + await this.createNewCipher(name, data.userName); // We are bypassing user verification pending approval. this.send({ @@ -310,6 +310,7 @@ export class Fido2Component implements OnInit, OnDestroy { name: data.credentialName || data.rpId, uri: this.url, uilocation: "popout", + username: data.userName, senderTabId: this.senderTabId, sessionId: this.sessionId, userVerification: data.userVerification, @@ -357,11 +358,13 @@ export class Fido2Component implements OnInit, OnDestroy { this.destroy$.complete(); } - private buildCipher(name: string) { + private buildCipher(name: string, username: string) { this.cipher = new CipherView(); this.cipher.name = name; + this.cipher.type = CipherType.Login; this.cipher.login = new LoginView(); + this.cipher.login.username = username; this.cipher.login.uris = [new LoginUriView()]; this.cipher.login.uris[0].uri = this.url; this.cipher.card = new CardView(); @@ -371,8 +374,8 @@ export class Fido2Component implements OnInit, OnDestroy { this.cipher.reprompt = CipherRepromptType.None; } - private async createNewCipher(name: string) { - this.buildCipher(name); + private async createNewCipher(name: string, username: string) { + this.buildCipher(name, username); const cipher = await this.cipherService.encrypt(this.cipher); try { await this.cipherService.createWithServer(cipher); diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html index 09b764cbc8f..4d8461a57c3 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.html @@ -1,8 +1,21 @@ + + + + - diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index a3fad87c1b1..52f174f9e5e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -1,54 +1,176 @@ -import { CommonModule } from "@angular/common"; +import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { ActivatedRoute } from "@angular/router"; +import { ActivatedRoute, Params } from "@angular/router"; +import { map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { SearchModule, ButtonModule } from "@bitwarden/components"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components"; +import { + CipherFormConfig, + CipherFormConfigService, + CipherFormMode, + CipherFormModule, + DefaultCipherFormConfigService, +} from "@bitwarden/vault"; 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 { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component"; + +/** + * Helper class to parse query parameters for the AddEdit route. + */ +class QueryParams { + constructor(params: Params) { + this.cipherId = params.cipherId; + this.type = parseInt(params.type, null); + this.clone = params.clone === "true"; + this.folderId = params.folderId; + this.organizationId = params.organizationId; + this.collectionId = params.collectionId; + this.uri = params.uri; + } + + /** + * The ID of the cipher to edit or clone. + */ + cipherId?: CipherId; + + /** + * The type of cipher to create. + */ + type: CipherType; + + /** + * Whether to clone the cipher. + */ + clone?: boolean; + + /** + * Optional folderId to pre-select. + */ + folderId?: string; + + /** + * Optional organizationId to pre-select. + */ + organizationId?: OrganizationId; + + /** + * Optional collectionId to pre-select. + */ + collectionId?: CollectionId; + + /** + * Optional URI to pre-fill for login ciphers. + */ + uri?: string; +} + +export type AddEditQueryParams = Partial>; @Component({ selector: "app-add-edit-v2", templateUrl: "add-edit-v2.component.html", standalone: true, + providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }], imports: [ CommonModule, SearchModule, JslibModule, FormsModule, ButtonModule, + OpenAttachmentsComponent, PopupPageComponent, PopupHeaderComponent, PopupFooterComponent, + CipherFormModule, + AsyncActionsModule, ], }) export class AddEditV2Component { headerText: string; + config: CipherFormConfig; + + get loading() { + return this.config == null; + } + + get originalCipherId(): CipherId | null { + return this.config?.originalCipher?.id as CipherId; + } constructor( private route: ActivatedRoute, + private location: Location, private i18nService: I18nService, + private addEditFormConfigService: CipherFormConfigService, ) { this.subscribeToParams(); } - subscribeToParams(): void { - this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => { - const isNew = params.isNew.toLowerCase() === "true"; - const cipherType = parseInt(params.type); - - this.headerText = this.setHeader(isNew, cipherType); - }); + onCipherSaved(savedCipher: CipherView) { + this.location.back(); } - setHeader(isNew: boolean, type: CipherType) { - const partOne = isNew ? "newItemHeader" : "editItemHeader"; + subscribeToParams(): void { + this.route.queryParams + .pipe( + takeUntilDestroyed(), + map((params) => new QueryParams(params)), + switchMap(async (params) => { + let mode: CipherFormMode; + if (params.cipherId == null) { + mode = "add"; + } else { + mode = params.clone ? "clone" : "edit"; + } + const config = await this.addEditFormConfigService.buildConfig( + mode, + params.cipherId, + params.type, + ); + + if (config.mode === "edit" && !config.originalCipher.edit) { + config.mode = "partial-edit"; + } + + this.setInitialValuesFromParams(params, config); + + return config; + }), + ) + .subscribe((config) => { + this.config = config; + this.headerText = this.setHeader(config.mode, config.cipherType); + }); + } + + setInitialValuesFromParams(params: QueryParams, config: CipherFormConfig) { + config.initialValues = {}; + if (params.folderId) { + config.initialValues.folderId = params.folderId; + } + if (params.organizationId) { + config.initialValues.organizationId = params.organizationId; + } + if (params.collectionId) { + config.initialValues.collectionIds = [params.collectionId]; + } + if (params.uri) { + config.initialValues.loginUri = params.uri; + } + } + + setHeader(mode: CipherFormMode, type: CipherType) { + const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; switch (type) { case CipherType.Login: diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html new file mode 100644 index 00000000000..e7c59df21f4 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.html @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts new file mode 100644 index 00000000000..0f09d12db9f --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.spec.ts @@ -0,0 +1,122 @@ +import { Component, Input } from "@angular/core"; +import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { ActivatedRoute, Router } from "@angular/router"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { ButtonComponent } from "@bitwarden/components"; + +import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component"; +import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component"; + +import { AttachmentsV2Component } from "./attachments-v2.component"; +import { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachments.component"; + +@Component({ + standalone: true, + selector: "popup-header", + template: ``, +}) +class MockPopupHeaderComponent { + @Input() pageTitle: string; +} + +@Component({ + standalone: true, + selector: "popup-footer", + template: ``, +}) +class MockPopupFooterComponent { + @Input() pageTitle: string; +} + +describe("AttachmentsV2Component", () => { + let component: AttachmentsV2Component; + let fixture: ComponentFixture; + const queryParams = new BehaviorSubject<{ cipherId: string }>({ cipherId: "5555-444-3333" }); + let cipherAttachment: CipherAttachmentsComponent; + const navigate = jest.fn(); + + const cipherDomain = { + type: CipherType.Login, + name: "Test Login", + }; + + const cipherServiceGet = jest.fn().mockResolvedValue(cipherDomain); + + beforeEach(async () => { + cipherServiceGet.mockClear(); + navigate.mockClear(); + + await TestBed.configureTestingModule({ + imports: [AttachmentsV2Component], + providers: [ + { provide: LogService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: Router, useValue: { navigate } }, + { + provide: ActivatedRoute, + useValue: { + queryParams, + }, + }, + { + provide: CipherService, + useValue: { + get: cipherServiceGet, + }, + }, + ], + }) + .overrideComponent(AttachmentsV2Component, { + remove: { + imports: [PopupHeaderComponent, PopupFooterComponent], + }, + add: { + imports: [MockPopupHeaderComponent, MockPopupFooterComponent], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AttachmentsV2Component); + component = fixture.componentInstance; + fixture.detectChanges(); + + cipherAttachment = fixture.debugElement.query( + By.directive(CipherAttachmentsComponent), + ).componentInstance; + }); + + it("sets `cipherId` from query params", () => { + expect(component.cipherId).toBe("5555-444-3333"); + }); + + it("passes the submit button to the cipher attachments component", () => { + const submitBtn = fixture.debugElement.queryAll(By.directive(ButtonComponent))[1] + .componentInstance; + + expect(cipherAttachment.submitBtn).toEqual(submitBtn); + }); + + it("navigates the user to the edit view `onUploadSuccess`", fakeAsync(() => { + cipherAttachment.onUploadSuccess.emit(); + + tick(); + + expect(navigate).toHaveBeenCalledWith(["/edit-cipher"], { + queryParams: { cipherId: "5555-444-3333", type: CipherType.Login }, + replaceUrl: true, + }); + })); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts new file mode 100644 index 00000000000..da0def529c2 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/attachments-v2.component.ts @@ -0,0 +1,62 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { ActivatedRoute, Router } from "@angular/router"; +import { first } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { ButtonModule } from "@bitwarden/components"; + +import { PopOutComponent } from "../../../../../platform/popup/components/pop-out.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 { CipherAttachmentsComponent } from "./cipher-attachments/cipher-attachments.component"; + +@Component({ + standalone: true, + selector: "app-attachments-v2", + templateUrl: "./attachments-v2.component.html", + imports: [ + CommonModule, + ButtonModule, + JslibModule, + CipherAttachmentsComponent, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + PopOutComponent, + ], +}) +export class AttachmentsV2Component { + /** The `id` tied to the underlying HTMLFormElement */ + attachmentFormId = CipherAttachmentsComponent.attachmentFormID; + + /** Id of the cipher */ + cipherId: CipherId; + + constructor( + private router: Router, + private cipherService: CipherService, + route: ActivatedRoute, + ) { + route.queryParams.pipe(takeUntilDestroyed(), first()).subscribe(({ cipherId }) => { + this.cipherId = cipherId; + }); + } + + /** Navigate the user back to the edit screen after uploading an attachment */ + async navigateToEditScreen() { + const cipherDomain = await this.cipherService.get(this.cipherId); + + void this.router.navigate(["/edit-cipher"], { + queryParams: { cipherId: this.cipherId, type: cipherDomain.type }, + // "replaceUrl" so the /attachments route is not in the history, thus when a back button + // is clicked, the user is taken to the view screen instead of the attachments screen + replaceUrl: true, + }); + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.html new file mode 100644 index 00000000000..159cab31e86 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.html @@ -0,0 +1,72 @@ +

{{ "attachments" | i18n }}

+ +
    +
  • + + + {{ attachment.fileName }} + {{ attachment.sizeName }} + + + + + + + + + + +
  • +
+ +
+ + +
+ + + + + +
+

+ {{ "maxFileSizeSansPunctuation" | i18n }} +

+
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.spec.ts new file mode 100644 index 00000000000..42c6c530eec --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.spec.ts @@ -0,0 +1,273 @@ +import { Component, Input } from "@angular/core"; +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; + +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 { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { ButtonComponent, ToastService } from "@bitwarden/components"; + +import { CipherAttachmentsComponent } from "./cipher-attachments.component"; +import { DeleteAttachmentComponent } from "./delete-attachment/delete-attachment.component"; +import { DownloadAttachmentComponent } from "./download-attachment/download-attachment.component"; + +@Component({ + standalone: true, + selector: "app-download-attachment", + template: "", +}) +class MockDownloadAttachmentComponent { + @Input() attachment: AttachmentView; + @Input() cipher: CipherView; +} + +describe("CipherAttachmentsComponent", () => { + let component: CipherAttachmentsComponent; + let fixture: ComponentFixture; + const showToast = jest.fn(); + const cipherView = { + id: "5555-444-3333", + type: CipherType.Login, + name: "Test Login", + login: { + username: "username", + password: "password", + }, + } as CipherView; + + const cipherDomain = { + decrypt: () => cipherView, + }; + + const cipherServiceGet = jest.fn().mockResolvedValue(cipherDomain); + const saveAttachmentWithServer = jest.fn().mockResolvedValue(cipherDomain); + + beforeEach(async () => { + cipherServiceGet.mockClear(); + showToast.mockClear(); + saveAttachmentWithServer.mockClear().mockResolvedValue(cipherDomain); + + await TestBed.configureTestingModule({ + imports: [CipherAttachmentsComponent], + providers: [ + { + provide: CipherService, + useValue: { + get: cipherServiceGet, + saveAttachmentWithServer, + getKeyForCipherKeyDecryption: () => Promise.resolve(null), + }, + }, + { + provide: ToastService, + useValue: { + showToast, + }, + }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: LogService, useValue: mock() }, + { provide: ConfigService, useValue: mock() }, + { provide: PlatformUtilsService, useValue: mock() }, + ], + }) + .overrideComponent(CipherAttachmentsComponent, { + remove: { + imports: [DownloadAttachmentComponent], + }, + add: { + imports: [MockDownloadAttachmentComponent], + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CipherAttachmentsComponent); + component = fixture.componentInstance; + component.cipherId = "5555-444-3333" as CipherId; + component.submitBtn = {} as ButtonComponent; + fixture.detectChanges(); + }); + + it("fetches cipherView using `cipherId`", async () => { + await component.ngOnInit(); + + expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333"); + expect(component.cipher).toEqual(cipherView); + }); + + it("sets testids for automation testing", () => { + const attachment = { + id: "1234-5678", + fileName: "test file.txt", + sizeName: "244.2 KB", + } as AttachmentView; + + component.cipher.attachments = [attachment]; + + fixture.detectChanges(); + + const fileName = fixture.debugElement.query(By.css('[data-testid="file-name"]')); + const fileSize = fixture.debugElement.query(By.css('[data-testid="file-size"]')); + + expect(fileName.nativeElement.textContent).toEqual(attachment.fileName); + expect(fileSize.nativeElement.textContent).toEqual(attachment.sizeName); + }); + + describe("bitSubmit", () => { + beforeEach(() => { + component.submitBtn.disabled = undefined; + component.submitBtn.loading = undefined; + }); + + it("updates sets initial state of the submit button", async () => { + await component.ngOnInit(); + + expect(component.submitBtn.disabled).toBe(true); + }); + + it("sets submitBtn loading state", () => { + component.bitSubmit.loading = true; + + expect(component.submitBtn.loading).toBe(true); + + component.bitSubmit.loading = false; + + expect(component.submitBtn.loading).toBe(false); + }); + + it("sets submitBtn disabled state", () => { + component.bitSubmit.disabled = true; + + expect(component.submitBtn.disabled).toBe(true); + + component.bitSubmit.disabled = false; + + expect(component.submitBtn.disabled).toBe(false); + }); + }); + + describe("attachmentForm", () => { + let file: File; + + beforeEach(() => { + component.submitBtn.disabled = undefined; + file = new File([""], "attachment.txt", { type: "text/plain" }); + + const inputElement = fixture.debugElement.query(By.css("input[type=file]")); + + // Set the file value of the input element + Object.defineProperty(inputElement.nativeElement, "files", { + value: [file], + writable: false, + }); + + // Trigger change event, for event listeners + inputElement.nativeElement.dispatchEvent(new InputEvent("change")); + }); + + it("sets value of `file` control when input changes", () => { + expect(component.attachmentForm.controls.file.value.name).toEqual(file.name); + }); + + it("updates disabled state of submit button", () => { + expect(component.submitBtn.disabled).toBe(false); + }); + }); + + describe("submit", () => { + describe("error", () => { + it("shows error toast if no file is selected", async () => { + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + title: "errorOccurred", + message: "selectFile", + }); + }); + + it("shows error toast if file size is greater than 500MB", async () => { + component.attachmentForm.controls.file.setValue({ + size: 524288001, + } as File); + + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + title: "errorOccurred", + message: "maxFileSize", + }); + }); + }); + + describe("success", () => { + const file = { size: 524287999 } as File; + + beforeEach(() => { + component.attachmentForm.controls.file.setValue(file); + }); + + it("calls `saveAttachmentWithServer`", async () => { + await component.submit(); + + expect(saveAttachmentWithServer).toHaveBeenCalledWith(cipherDomain, file); + }); + + it("resets form and input values", async () => { + await component.submit(); + + const fileInput = fixture.debugElement.query(By.css("input[type=file]")); + + expect(fileInput.nativeElement.value).toEqual(""); + expect(component.attachmentForm.controls.file.value).toEqual(null); + }); + + it("shows success toast", async () => { + await component.submit(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "attachmentSaved", + }); + }); + + it('emits "onUploadSuccess"', async () => { + const emitSpy = jest.spyOn(component.onUploadSuccess, "emit"); + + await component.submit(); + + expect(emitSpy).toHaveBeenCalled(); + }); + }); + }); + + describe("removeAttachment", () => { + const attachment = { id: "1234-5678" } as AttachmentView; + + beforeEach(() => { + component.cipher.attachments = [attachment]; + + fixture.detectChanges(); + }); + + it("removes attachment from cipher", () => { + const deleteAttachmentComponent = fixture.debugElement.query( + By.directive(DeleteAttachmentComponent), + ).componentInstance as DeleteAttachmentComponent; + + deleteAttachmentComponent.onDeletionSuccess.emit(); + + expect(component.cipher.attachments).toEqual([]); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.ts new file mode 100644 index 00000000000..21115955653 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/cipher-attachments.component.ts @@ -0,0 +1,211 @@ +import { CommonModule } from "@angular/common"; +import { + AfterViewInit, + Component, + DestroyRef, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, + inject, +} from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { + AsyncActionsModule, + BitSubmitDirective, + ButtonComponent, + ButtonModule, + CardComponent, + ItemModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; + +import { DeleteAttachmentComponent } from "./delete-attachment/delete-attachment.component"; +import { DownloadAttachmentComponent } from "./download-attachment/download-attachment.component"; + +type CipherAttachmentForm = FormGroup<{ + file: FormControl; +}>; + +@Component({ + standalone: true, + selector: "app-cipher-attachments", + templateUrl: "./cipher-attachments.component.html", + imports: [ + AsyncActionsModule, + ButtonModule, + CommonModule, + ItemModule, + JslibModule, + ReactiveFormsModule, + TypographyModule, + CardComponent, + DeleteAttachmentComponent, + DownloadAttachmentComponent, + ], +}) +export class CipherAttachmentsComponent implements OnInit, AfterViewInit { + /** `id` associated with the form element */ + static attachmentFormID = "attachmentForm"; + + /** Reference to the file HTMLInputElement */ + @ViewChild("fileInput", { read: ElementRef }) private fileInput: ElementRef; + + /** Reference to the BitSubmitDirective */ + @ViewChild(BitSubmitDirective) bitSubmit: BitSubmitDirective; + + /** The `id` of the cipher in context */ + @Input({ required: true }) cipherId: CipherId; + + /** An optional submit button, whose loading/disabled state will be tied to the form state. */ + @Input() submitBtn?: ButtonComponent; + + /** Emits after a file has been successfully uploaded */ + @Output() onUploadSuccess = new EventEmitter(); + + cipher: CipherView; + + attachmentForm: CipherAttachmentForm = this.formBuilder.group({ + file: new FormControl(null, [Validators.required]), + }); + + private cipherDomain: Cipher; + private destroy$ = inject(DestroyRef); + + constructor( + private cipherService: CipherService, + private i18nService: I18nService, + private formBuilder: FormBuilder, + private logService: LogService, + private toastService: ToastService, + ) { + this.attachmentForm.statusChanges.pipe(takeUntilDestroyed()).subscribe((status) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.disabled = status !== "VALID"; + }); + } + + async ngOnInit(): Promise { + this.cipherDomain = await this.cipherService.get(this.cipherId); + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain), + ); + + // Update the initial state of the submit button + if (this.submitBtn) { + this.submitBtn.disabled = !this.attachmentForm.valid; + } + } + + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntilDestroyed(this.destroy$)).subscribe((loading) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.loading = loading; + }); + + this.bitSubmit.disabled$.pipe(takeUntilDestroyed(this.destroy$)).subscribe((disabled) => { + if (!this.submitBtn) { + return; + } + + this.submitBtn.disabled = disabled; + }); + } + + /** Reference the `id` via the static property */ + get attachmentFormId(): string { + return CipherAttachmentsComponent.attachmentFormID; + } + + /** Updates the form value when a file is selected */ + onFileChange(event: Event): void { + const fileInputEl = event.target as HTMLInputElement; + + if (fileInputEl.files && fileInputEl.files.length > 0) { + this.attachmentForm.controls.file.setValue(fileInputEl.files[0]); + } + } + + /** Save the attachments to the cipher */ + submit = async () => { + const file = this.attachmentForm.value.file; + if (file === null) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("selectFile"), + }); + return; + } + + if (file.size > 524288000) { + // 500 MB + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("maxFileSize"), + }); + return; + } + + try { + this.cipherDomain = await this.cipherService.saveAttachmentWithServer( + this.cipherDomain, + file, + ); + + // re-decrypt the cipher to update the attachments + this.cipher = await this.cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain), + ); + + // Reset reactive form and input element + this.fileInput.nativeElement.value = ""; + this.attachmentForm.controls.file.setValue(null); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("attachmentSaved"), + }); + + this.onUploadSuccess.emit(); + } catch (e) { + this.logService.error(e); + } + }; + + /** Removes the attachment from the cipher */ + removeAttachment(attachment: AttachmentView) { + const index = this.cipher.attachments.indexOf(attachment); + + if (index > -1) { + this.cipher.attachments.splice(index, 1); + } + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.html new file mode 100644 index 00000000000..38ece650b72 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.html @@ -0,0 +1,9 @@ + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.spec.ts new file mode 100644 index 00000000000..749093902d7 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.spec.ts @@ -0,0 +1,105 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; + +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 { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { DialogService, ToastService } from "@bitwarden/components"; + +import { DeleteAttachmentComponent } from "./delete-attachment.component"; + +describe("DeleteAttachmentComponent", () => { + let component: DeleteAttachmentComponent; + let fixture: ComponentFixture; + const showToast = jest.fn(); + const attachment = { + id: "222-3333-4444", + url: "attachment-url", + fileName: "attachment-filename", + size: "1234", + } as AttachmentView; + + const deleteAttachmentWithServer = jest.fn().mockResolvedValue(null); + const openSimpleDialog = jest.fn().mockResolvedValue(true); + + beforeEach(async () => { + deleteAttachmentWithServer.mockClear(); + showToast.mockClear(); + openSimpleDialog.mockClear().mockResolvedValue(true); + + await TestBed.configureTestingModule({ + imports: [DeleteAttachmentComponent], + providers: [ + { + provide: CipherService, + useValue: { deleteAttachmentWithServer }, + }, + { + provide: ToastService, + useValue: { showToast }, + }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: LogService, useValue: mock() }, + ], + }) + .overrideProvider(DialogService, { + useValue: { + openSimpleDialog, + }, + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DeleteAttachmentComponent); + component = fixture.componentInstance; + component.cipherId = "5555-444-3333"; + component.attachment = attachment; + fixture.detectChanges(); + }); + + it("renders delete button", () => { + const deleteButton = fixture.debugElement.query(By.css("button")); + + expect(deleteButton.attributes["title"]).toBe("deleteAttachmentName"); + }); + + it("does not delete when the user cancels the dialog", async () => { + openSimpleDialog.mockResolvedValue(false); + + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "deleteAttachment" }, + content: { key: "permanentlyDeleteAttachmentConfirmation" }, + type: "warning", + }); + + expect(deleteAttachmentWithServer).not.toHaveBeenCalled(); + }); + + it("deletes the attachment", async () => { + await component.delete(); + + expect(openSimpleDialog).toHaveBeenCalledWith({ + title: { key: "deleteAttachment" }, + content: { key: "permanentlyDeleteAttachmentConfirmation" }, + type: "warning", + }); + + // Called with cipher id and attachment id + expect(deleteAttachmentWithServer).toHaveBeenCalledWith("5555-444-3333", "222-3333-4444"); + }); + + it("shows toast message on successful deletion", async () => { + await component.delete(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: "deletedAttachment", + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.ts new file mode 100644 index 00000000000..932b2df2e17 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/delete-attachment/delete-attachment.component.ts @@ -0,0 +1,66 @@ +import { CommonModule } from "@angular/common"; +import { Component, EventEmitter, Input, Output } from "@angular/core"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +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 { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { + AsyncActionsModule, + ButtonModule, + DialogService, + IconButtonModule, + ToastService, +} from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-delete-attachment", + templateUrl: "./delete-attachment.component.html", + imports: [AsyncActionsModule, CommonModule, JslibModule, ButtonModule, IconButtonModule], +}) +export class DeleteAttachmentComponent { + /** Id of the cipher associated with the attachment */ + @Input({ required: true }) cipherId: string; + + /** The attachment that is can be deleted */ + @Input({ required: true }) attachment: AttachmentView; + + /** Emits when the attachment is successfully deleted */ + @Output() onDeletionSuccess = new EventEmitter(); + + constructor( + private toastService: ToastService, + private i18nService: I18nService, + private cipherService: CipherService, + private logService: LogService, + private dialogService: DialogService, + ) {} + + delete = async () => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteAttachment" }, + content: { key: "permanentlyDeleteAttachmentConfirmation" }, + type: "warning", + }); + + if (!confirmed) { + return; + } + + try { + await this.cipherService.deleteAttachmentWithServer(this.cipherId, this.attachment.id); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedAttachment"), + }); + + this.onDeletionSuccess.emit(); + } catch (e) { + this.logService.error(e); + } + }; +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.html new file mode 100644 index 00000000000..e6a20ba044b --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.html @@ -0,0 +1,8 @@ + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.spec.ts new file mode 100644 index 00000000000..45c9e7fb377 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.spec.ts @@ -0,0 +1,144 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { By } from "@angular/platform-browser"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { CipherType } from "@bitwarden/common/vault/enums"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { ToastService } from "../../../../../../../../../../libs/components/src/toast"; + +import { DownloadAttachmentComponent } from "./download-attachment.component"; + +class MockRequest { + constructor(public url: string) {} +} + +describe("DownloadAttachmentComponent", () => { + let component: DownloadAttachmentComponent; + let fixture: ComponentFixture; + const activeUserId$ = new BehaviorSubject("888-333-222-222"); + const showToast = jest.fn(); + const getAttachmentData = jest + .fn() + .mockResolvedValue({ url: "https://www.downloadattachement.com" }); + const download = jest.fn(); + + const attachment = { + id: "222-3333-4444", + url: "https://www.attachment.com", + fileName: "attachment-filename", + size: "1234", + } as AttachmentView; + + const cipherView = { + id: "5555-444-3333", + type: CipherType.Login, + name: "Test Login", + login: { + username: "username", + password: "password", + }, + } as CipherView; + + beforeEach(async () => { + showToast.mockClear(); + getAttachmentData.mockClear(); + download.mockClear(); + + await TestBed.configureTestingModule({ + imports: [DownloadAttachmentComponent], + providers: [ + { provide: EncryptService, useValue: mock() }, + { provide: CryptoService, useValue: mock() }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: StateProvider, useValue: { activeUserId$ } }, + { provide: ToastService, useValue: { showToast } }, + { provide: ApiService, useValue: { getAttachmentData } }, + { provide: FileDownloadService, useValue: { download } }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DownloadAttachmentComponent); + component = fixture.componentInstance; + component.attachment = attachment; + component.cipher = cipherView; + fixture.detectChanges(); + }); + + it("renders delete button", () => { + const deleteButton = fixture.debugElement.query(By.css("button")); + + expect(deleteButton.attributes["title"]).toBe("downloadAttachmentName"); + }); + + describe("download attachment", () => { + let fetchMock: jest.Mock; + + beforeEach(() => { + fetchMock = jest.fn().mockResolvedValue({}); + global.fetch = fetchMock; + // Request is not defined in the Jest runtime + // eslint-disable-next-line no-global-assign + Request = MockRequest as any; + }); + + it("uses the attachment url when available when getAttachmentData returns a 404", async () => { + getAttachmentData.mockRejectedValue(new ErrorResponse({}, 404)); + + await component.download(); + + expect(fetchMock).toHaveBeenCalledWith({ url: attachment.url }); + }); + + it("calls file download service with the attachment url", async () => { + getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" }); + fetchMock.mockResolvedValue({ status: 200 }); + EncArrayBuffer.fromResponse = jest.fn().mockResolvedValue({}); + + await component.download(); + + expect(download).toHaveBeenCalledWith({ blobData: undefined, fileName: attachment.fileName }); + }); + + describe("errors", () => { + it("shows an error toast when fetch fails", async () => { + getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" }); + fetchMock.mockResolvedValue({ status: 500 }); + + await component.download(); + + expect(showToast).toHaveBeenCalledWith({ + message: "errorOccurred", + title: null, + variant: "error", + }); + }); + + it("shows an error toast when EncArrayBuffer fails", async () => { + getAttachmentData.mockResolvedValue({ url: "https://www.downloadattachement.com" }); + fetchMock.mockResolvedValue({ status: 200 }); + EncArrayBuffer.fromResponse = jest.fn().mockRejectedValue({}); + + await component.download(); + + expect(showToast).toHaveBeenCalledWith({ + message: "errorOccurred", + title: null, + variant: "error", + }); + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.ts new file mode 100644 index 00000000000..528695eab45 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/cipher-attachments/download-attachment/download-attachment.component.ts @@ -0,0 +1,104 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { NEVER, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; +import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; +import { StateProvider } from "@bitwarden/common/platform/state"; +import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey } from "@bitwarden/common/types/key"; +import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { AsyncActionsModule, IconButtonModule, ToastService } from "@bitwarden/components"; + +@Component({ + standalone: true, + selector: "app-download-attachment", + templateUrl: "./download-attachment.component.html", + imports: [AsyncActionsModule, CommonModule, JslibModule, IconButtonModule], +}) +export class DownloadAttachmentComponent { + /** Attachment to download */ + @Input({ required: true }) attachment: AttachmentView; + + /** The cipher associated with the attachment */ + @Input({ required: true }) cipher: CipherView; + + /** The organization key if the cipher is associated with one */ + private orgKey: OrgKey | null = null; + + constructor( + private i18nService: I18nService, + private apiService: ApiService, + private fileDownloadService: FileDownloadService, + private toastService: ToastService, + private encryptService: EncryptService, + private stateProvider: StateProvider, + private cryptoService: CryptoService, + ) { + this.stateProvider.activeUserId$ + .pipe( + switchMap((userId) => (userId !== null ? this.cryptoService.orgKeys$(userId) : NEVER)), + takeUntilDestroyed(), + ) + .subscribe((data: Record | null) => { + if (data) { + this.orgKey = data[this.cipher.organizationId as OrganizationId]; + } + }); + } + + /** Download the attachment */ + download = async () => { + let url: string; + + try { + const attachmentDownloadResponse = await this.apiService.getAttachmentData( + this.cipher.id, + this.attachment.id, + ); + url = attachmentDownloadResponse.url; + } catch (e) { + if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) { + url = this.attachment.url; + } else if (e instanceof ErrorResponse) { + throw new Error((e as ErrorResponse).getSingleMessage()); + } else { + throw e; + } + } + + const response = await fetch(new Request(url, { cache: "no-store" })); + if (response.status !== 200) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); + return; + } + + try { + const encBuf = await EncArrayBuffer.fromResponse(response); + const key = this.attachment.key != null ? this.attachment.key : this.orgKey; + const decBuf = await this.encryptService.decryptToBytes(encBuf, key); + this.fileDownloadService.download({ + fileName: this.attachment.fileName, + blobData: decBuf, + }); + } catch (e) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("errorOccurred"), + }); + } + }; +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html new file mode 100644 index 00000000000..6b2d8eaa033 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.html @@ -0,0 +1,14 @@ + + + diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts new file mode 100644 index 00000000000..d819392e678 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.spec.ts @@ -0,0 +1,177 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { Router } from "@angular/router"; +import { RouterTestingModule } from "@angular/router/testing"; +import { BehaviorSubject } from "rxjs"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId } 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"; +import { ToastService } from "@bitwarden/components"; + +import BrowserPopupUtils from "../../../../../../platform/popup/browser-popup-utils"; +import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; + +import { OpenAttachmentsComponent } from "./open-attachments.component"; + +describe("OpenAttachmentsComponent", () => { + let component: OpenAttachmentsComponent; + let fixture: ComponentFixture; + let router: Router; + const showToast = jest.fn(); + const hasPremiumFromAnySource$ = new BehaviorSubject(true); + const openCurrentPagePopout = jest + .spyOn(BrowserPopupUtils, "openCurrentPagePopout") + .mockResolvedValue(null); + const cipherView = { + id: "5555-444-3333", + type: CipherType.Login, + name: "Test Login", + login: { + username: "username", + password: "password", + }, + } as CipherView; + + const cipherDomain = { + decrypt: () => cipherView, + }; + + const org = { + name: "Test Org", + productTierType: ProductTierType.Enterprise, + } as Organization; + + const getCipher = jest.fn().mockResolvedValue(cipherDomain); + const getOrganization = jest.fn().mockResolvedValue(org); + const showFilePopoutMessage = jest.fn().mockReturnValue(false); + + beforeEach(async () => { + openCurrentPagePopout.mockClear(); + getCipher.mockClear(); + showToast.mockClear(); + getOrganization.mockClear(); + showFilePopoutMessage.mockClear(); + + await TestBed.configureTestingModule({ + imports: [OpenAttachmentsComponent, RouterTestingModule], + providers: [ + { provide: I18nService, useValue: { t: (key: string) => key } }, + { provide: BillingAccountProfileStateService, useValue: { hasPremiumFromAnySource$ } }, + { + provide: CipherService, + useValue: { + get: getCipher, + getKeyForCipherKeyDecryption: () => Promise.resolve(null), + }, + }, + { + provide: ToastService, + useValue: { showToast }, + }, + { + provide: OrganizationService, + useValue: { get: getOrganization }, + }, + { + provide: FilePopoutUtilsService, + useValue: { showFilePopoutMessage }, + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OpenAttachmentsComponent); + component = fixture.componentInstance; + component.cipherId = "5555-444-3333" as CipherId; + router = TestBed.inject(Router); + jest.spyOn(router, "navigate").mockResolvedValue(true); + fixture.detectChanges(); + }); + + it("opens attachments in new popout", async () => { + showFilePopoutMessage.mockReturnValue(true); + + await component.ngOnInit(); + + await component.openAttachments(); + + expect(router.navigate).not.toHaveBeenCalled(); + expect(openCurrentPagePopout).toHaveBeenCalledWith( + window, + "http:/localhost//attachments?cipherId=5555-444-3333", + ); + }); + + it("opens attachments in same window", async () => { + showFilePopoutMessage.mockReturnValue(false); + + await component.ngOnInit(); + + await component.openAttachments(); + + expect(openCurrentPagePopout).not.toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(["/attachments"], { + queryParams: { cipherId: "5555-444-3333" }, + }); + }); + + it("routes the user to the premium page when they cannot access premium features", async () => { + hasPremiumFromAnySource$.next(false); + + await component.openAttachments(); + + expect(router.navigate).toHaveBeenCalledWith(["/premium"]); + }); + + describe("Free Orgs", () => { + beforeEach(() => { + component.cipherIsAPartOfFreeOrg = undefined; + }); + + it("sets `cipherIsAPartOfFreeOrg` to false when the cipher is not a part of an organization", async () => { + cipherView.organizationId = null; + + await component.ngOnInit(); + + expect(component.cipherIsAPartOfFreeOrg).toBe(false); + }); + + it("sets `cipherIsAPartOfFreeOrg` to true when the cipher is a part of a free organization", async () => { + cipherView.organizationId = "888-333-333"; + org.productTierType = ProductTierType.Free; + + await component.ngOnInit(); + + expect(component.cipherIsAPartOfFreeOrg).toBe(true); + }); + + it("sets `cipherIsAPartOfFreeOrg` to false when the organization is not free", async () => { + cipherView.organizationId = "888-333-333"; + org.productTierType = ProductTierType.Families; + + await component.ngOnInit(); + + expect(component.cipherIsAPartOfFreeOrg).toBe(false); + }); + + it("shows toast when the cipher is a part of a free org", async () => { + component.canAccessAttachments = true; + component.cipherIsAPartOfFreeOrg = true; + + await component.openAttachments(); + + expect(showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: "freeOrgsCannotUseAttachments", + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts new file mode 100644 index 00000000000..89d483c1d01 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -0,0 +1,109 @@ +import { CommonModule } from "@angular/common"; +import { Component, Input, OnInit } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { Router } from "@angular/router"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; +import { ProductTierType } from "@bitwarden/common/billing/enums"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { + BadgeModule, + CardComponent, + ItemModule, + ToastService, + TypographyModule, +} from "@bitwarden/components"; + +import BrowserPopupUtils from "../../../../../../platform/popup/browser-popup-utils"; +import { FilePopoutUtilsService } from "../../../../../../tools/popup/services/file-popout-utils.service"; + +@Component({ + standalone: true, + selector: "app-open-attachments", + templateUrl: "./open-attachments.component.html", + imports: [BadgeModule, CommonModule, ItemModule, JslibModule, TypographyModule, CardComponent], +}) +export class OpenAttachmentsComponent implements OnInit { + /** Cipher `id` */ + @Input({ required: true }) cipherId: CipherId; + + /** True when the attachments window should be opened in a popout */ + openAttachmentsInPopout: boolean; + + /** True when the user has access to premium or h */ + canAccessAttachments: boolean; + + /** True when the cipher is a part of a free organization */ + cipherIsAPartOfFreeOrg: boolean; + + constructor( + private router: Router, + private billingAccountProfileStateService: BillingAccountProfileStateService, + private cipherService: CipherService, + private organizationService: OrganizationService, + private toastService: ToastService, + private i18nService: I18nService, + private filePopoutUtilsService: FilePopoutUtilsService, + ) { + this.billingAccountProfileStateService.hasPremiumFromAnySource$ + .pipe(takeUntilDestroyed()) + .subscribe((canAccessPremium) => { + this.canAccessAttachments = canAccessPremium; + }); + } + + async ngOnInit(): Promise { + this.openAttachmentsInPopout = this.filePopoutUtilsService.showFilePopoutMessage(window); + + if (!this.cipherId) { + return; + } + + const cipherDomain = await this.cipherService.get(this.cipherId); + const cipher = await cipherDomain.decrypt( + await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain), + ); + + if (!cipher.organizationId) { + this.cipherIsAPartOfFreeOrg = false; + return; + } + + const org = await this.organizationService.get(cipher.organizationId); + + this.cipherIsAPartOfFreeOrg = org.productTierType === ProductTierType.Free; + } + + /** Routes the user to the attachments screen, if available */ + async openAttachments() { + if (!this.canAccessAttachments) { + await this.router.navigate(["/premium"]); + return; + } + + if (this.cipherIsAPartOfFreeOrg) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("freeOrgsCannotUseAttachments"), + }); + return; + } + + if (this.openAttachmentsInPopout) { + const destinationUrl = this.router + .createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipherId } }) + .toString(); + + const currentBaseUrl = window.location.href.replace(this.router.url, ""); + + await BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl); + } else { + await this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipherId } }); + } + } +} diff --git a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts index eb8737d5139..8e72d84053d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/autofill-vault-list-items/autofill-vault-list-items.component.ts @@ -12,6 +12,7 @@ import { } from "@bitwarden/components"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; import { PopupCipherView } from "../../../views/popup-cipher.view"; import { VaultListItemsContainerComponent } from "../vault-list-items-container/vault-list-items-container.component"; @@ -53,7 +54,7 @@ export class AutofillVaultListItemsComponent { protected showEmptyAutofillTip$: Observable = combineLatest([ this.vaultPopupItemsService.hasFilterApplied$, this.autofillCiphers$, - this.vaultPopupItemsService.autofillAllowed$, + this.vaultPopupAutofillService.autofillAllowed$, ]).pipe( map( ([hasFilter, ciphers, canAutoFill]) => @@ -61,7 +62,10 @@ export class AutofillVaultListItemsComponent { ), ); - constructor(private vaultPopupItemsService: VaultPopupItemsService) { + constructor( + private vaultPopupItemsService: VaultPopupItemsService, + private vaultPopupAutofillService: VaultPopupAutofillService, + ) { // TODO: Migrate logic to show Autofill policy toast PM-8144 } @@ -70,6 +74,6 @@ export class AutofillVaultListItemsComponent { * @protected */ protected refreshCurrentTab() { - this.vaultPopupItemsService.refreshCurrentTab(); + this.vaultPopupAutofillService.refreshCurrentTab(); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html index ef451bd9343..05a6b54d4d0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.html @@ -10,10 +10,10 @@ - - diff --git a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts index c137a51d72a..23ff9593099 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/item-more-options/item-more-options.component.ts @@ -18,7 +18,8 @@ import { PasswordRepromptService } from "@bitwarden/vault"; import { BrowserApi } from "../../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils"; -import { VaultPopupItemsService } from "../../../services/vault-popup-items.service"; +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; +import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; @Component({ standalone: true, @@ -39,16 +40,16 @@ export class ItemMoreOptionsComponent { @Input({ transform: booleanAttribute }) hideAutofillOptions: boolean; - protected autofillAllowed$ = this.vaultPopupItemsService.autofillAllowed$; + protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$; constructor( private cipherService: CipherService, - private vaultPopupItemsService: VaultPopupItemsService, private passwordRepromptService: PasswordRepromptService, private toastService: ToastService, private dialogService: DialogService, private router: Router, private i18nService: I18nService, + private vaultPopupAutofillService: VaultPopupAutofillService, ) {} get canEdit() { @@ -62,10 +63,22 @@ export class ItemMoreOptionsComponent { return [CipherType.Login, CipherType.Card, CipherType.Identity].includes(this.cipher.type); } + get isLogin() { + return this.cipher.type === CipherType.Login; + } + get favoriteText() { return this.cipher.favorite ? "unfavorite" : "favorite"; } + async doAutofill() { + await this.vaultPopupAutofillService.doAutofill(this.cipher); + } + + async doAutofillAndSave() { + await this.vaultPopupAutofillService.doAutofillAndSave(this.cipher); + } + /** * Determines if the login cipher can be launched in a new browser tab. */ @@ -133,9 +146,10 @@ export class ItemMoreOptionsComponent { await this.router.navigate(["/clone-cipher"], { queryParams: { - cloneMode: true, + clone: true.toString(), cipherId: this.cipher.id, - }, + type: this.cipher.type.toString(), + } as AddEditQueryParams, }); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts index e90afec5388..65456fd74ae 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/new-item-dropdown/new-item-dropdown-v2.component.ts @@ -1,10 +1,19 @@ import { CommonModule } from "@angular/common"; -import { Component, OnDestroy, OnInit } from "@angular/core"; +import { Component, Input } from "@angular/core"; import { Router, RouterLink } from "@angular/router"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; -import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components"; +import { ButtonModule, MenuModule, NoItemsModule } from "@bitwarden/components"; + +import { AddEditQueryParams } from "../add-edit/add-edit-v2.component"; + +export interface NewItemInitialValues { + folderId?: string; + organizationId?: OrganizationId; + collectionId?: CollectionId; +} @Component({ selector: "app-new-item-dropdown", @@ -12,17 +21,27 @@ import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components"; standalone: true, imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule], }) -export class NewItemDropdownV2Component implements OnInit, OnDestroy { +export class NewItemDropdownV2Component { cipherType = CipherType; + /** + * Optional initial values to pass to the add cipher form + */ + @Input() + initialValues: NewItemInitialValues; + constructor(private router: Router) {} - ngOnInit(): void {} + private buildQueryParams(type: CipherType): AddEditQueryParams { + return { + type: type.toString(), + collectionId: this.initialValues?.collectionId, + organizationId: this.initialValues?.organizationId, + folderId: this.initialValues?.folderId, + }; + } - ngOnDestroy(): void {} - - // TODO PM-6826: add selectedVault query param newItemNavigate(type: CipherType) { - void this.router.navigate(["/add-cipher"], { queryParams: { type: type, isNew: true } }); + void this.router.navigate(["/add-cipher"], { queryParams: this.buildQueryParams(type) }); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html index 90981204020..fbe1d60b448 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.html @@ -21,7 +21,7 @@ @@ -40,6 +40,7 @@ type="button" bitBadge variant="primary" + (click)="doAutofill(cipher)" [title]="'autofillTitle' | i18n: cipher.name" [attr.aria-label]="'autofillTitle' | i18n: cipher.name" > diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index 0c810756352..b6ba09fb315 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -14,6 +14,7 @@ import { TypographyModule, } from "@bitwarden/components"; +import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service"; import { PopupCipherView } from "../../../views/popup-cipher.view"; import { ItemCopyActionsComponent } from "../item-copy-action/item-copy-actions.component"; import { ItemMoreOptionsComponent } from "../item-more-options/item-more-options.component"; @@ -87,5 +88,12 @@ export class VaultListItemsContainerComponent { return cipher.collections[0]?.name; } - constructor(private i18nService: I18nService) {} + constructor( + private i18nService: I18nService, + private vaultPopupAutofillService: VaultPopupAutofillService, + ) {} + + async doAutofill(cipher: PopupCipherView) { + await this.vaultPopupAutofillService.doAutofill(cipher); + } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html new file mode 100644 index 00000000000..c2516fe05f7 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.html @@ -0,0 +1,20 @@ + + + + + + + + {{ "edit" | i18n }} + + + +
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts new file mode 100644 index 00000000000..4fe88da5550 --- /dev/null +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -0,0 +1,157 @@ +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormsModule } from "@angular/forms"; +import { ActivatedRoute, Router } from "@angular/router"; +import { Observable, switchMap } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +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 { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; +import { + AsyncActionsModule, + SearchModule, + ButtonModule, + IconButtonModule, + DialogService, + ToastService, +} from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { CipherViewComponent } from "../../../../../../../../libs/vault/src/cipher-view"; + +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"; + +@Component({ + selector: "app-view-v2", + templateUrl: "view-v2.component.html", + standalone: true, + imports: [ + CommonModule, + SearchModule, + JslibModule, + FormsModule, + ButtonModule, + PopupPageComponent, + PopupHeaderComponent, + PopupFooterComponent, + IconButtonModule, + CipherViewComponent, + AsyncActionsModule, + ], +}) +export class ViewV2Component { + headerText: string; + cipherId: string; + cipher: CipherView; + organization$: Observable; + folder$: Observable; + collections$: Observable; + private passwordReprompted = false; + + constructor( + private route: ActivatedRoute, + private router: Router, + private i18nService: I18nService, + private cipherService: CipherService, + private passwordRepromptService: PasswordRepromptService, + private dialogService: DialogService, + private logService: LogService, + private toastService: ToastService, + ) { + this.subscribeToParams(); + } + + subscribeToParams(): void { + this.route.queryParams + .pipe( + switchMap((param) => { + return this.getCipherData(param.cipherId); + }), + takeUntilDestroyed(), + ) + .subscribe((data) => { + this.cipher = data; + this.headerText = this.setHeader(data.type); + }); + } + + setHeader(type: CipherType) { + switch (type) { + case CipherType.Login: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin")); + case CipherType.Card: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard")); + case CipherType.Identity: + return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity")); + case CipherType.SecureNote: + return this.i18nService.t("viewItemHeader", this.i18nService.t("note")); + } + } + + async getCipherData(id: string) { + const cipher = await this.cipherService.get(id); + return await cipher.decrypt(await this.cipherService.getKeyForCipherKeyDecryption(cipher)); + } + + editCipher() { + if (this.cipher.isDeleted) { + return false; + } + void this.router.navigate(["/edit-cipher"], { + queryParams: { cipherId: this.cipher.id, type: this.cipher.type, isNew: false }, + }); + return true; + } + + delete = async (): Promise => { + this.passwordReprompted = + this.passwordReprompted || + (await this.passwordRepromptService.passwordRepromptCheck(this.cipher)); + if (!this.passwordReprompted) { + return; + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: this.cipher.isDeleted ? "permanentlyDeleteItemConfirmation" : "deleteItemConfirmation", + }, + type: "warning", + }); + + if (!confirmed) { + return false; + } + + try { + await this.deleteCipher(); + } catch (e) { + this.logService.error(e); + return false; + } + + await this.router.navigate(["/vault"]); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t(this.cipher.isDeleted ? "permanentlyDeletedItem" : "deletedItem"), + }); + + return true; + }; + + protected deleteCipher() { + return this.cipher.isDeleted + ? this.cipherService.deleteWithServer(this.cipher.id) + : this.cipherService.softDeleteWithServer(this.cipher.id); + } +} diff --git a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts index bf7a6b07a5c..e72077fa82d 100644 --- a/apps/browser/src/vault/popup/components/vault/add-edit.component.ts +++ b/apps/browser/src/vault/popup/components/vault/add-edit.component.ts @@ -128,6 +128,14 @@ export class AddEditComponent extends BaseAddEditComponent { await this.load(); if (!this.editMode || this.cloneMode) { + // Only allow setting username if there's no existing value + if ( + params.username && + (this.cipher.login.username == null || this.cipher.login.username === "") + ) { + this.cipher.login.username = params.username; + } + if (params.name && (this.cipher.name == null || this.cipher.name === "")) { this.cipher.name = params.name; } diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index abb810c04d5..df78806edf2 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -198,7 +198,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn super.selectCipher(cipher); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } }); + this.router.navigate(["/view-cipher"], { + queryParams: { cipherId: cipher.id }, + }); } this.preventSelected = false; }, 200); diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html index b97fa38eea9..f666bc09a89 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.html +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.html @@ -1,7 +1,7 @@ - + @@ -15,7 +15,10 @@ {{ "yourVaultIsEmpty" | i18n }} {{ "autofillSuggestionsTip" | i18n }} - + diff --git a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts index 9939727806b..777e44f0e16 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-v2.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from "@angular/common"; import { Component, OnDestroy, OnInit } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { Router, RouterLink } from "@angular/router"; -import { combineLatest } from "rxjs"; +import { RouterLink } from "@angular/router"; +import { combineLatest, map, Observable, shareReplay } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components"; @@ -13,8 +14,12 @@ import { PopOutComponent } from "../../../../platform/popup/components/pop-out.c import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component"; import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component"; import { VaultPopupItemsService } from "../../services/vault-popup-items.service"; +import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service"; import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2"; -import { NewItemDropdownV2Component } from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component"; +import { + NewItemDropdownV2Component, + NewItemInitialValues, +} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component"; import { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component"; import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component"; @@ -50,6 +55,17 @@ export class VaultV2Component implements OnInit, OnDestroy { protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$; protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$; + protected newItemItemValues$: Observable = + this.vaultPopupListFiltersService.filters$.pipe( + map((filter) => ({ + organizationId: (filter.organization?.id || + filter.collection?.organizationId) as OrganizationId, + collectionId: filter.collection?.id as CollectionId, + folderId: filter.folder?.id, + })), + shareReplay({ refCount: true, bufferSize: 1 }), + ); + /** Visual state of the vault */ protected vaultState: VaultState | null = null; @@ -61,7 +77,7 @@ export class VaultV2Component implements OnInit, OnDestroy { constructor( private vaultPopupItemsService: VaultPopupItemsService, - private router: Router, + private vaultPopupListFiltersService: VaultPopupListFiltersService, ) { combineLatest([ this.vaultPopupItemsService.emptyVault$, diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts new file mode 100644 index 00000000000..6e74fd7c231 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.spec.ts @@ -0,0 +1,356 @@ +import { TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +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 { subscribeTo } from "@bitwarden/common/spec"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { + AutoFillOptions, + AutofillService, + PageDetail, +} from "../../../autofill/services/abstractions/autofill.service"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; + +describe("VaultPopupAutofillService", () => { + let testBed: TestBed; + let service: VaultPopupAutofillService; + + const mockCurrentTab = { url: "https://example.com" } as chrome.tabs.Tab; + + // Create mocks for VaultPopupAutofillService + const mockAutofillService = mock(); + const mockI18nService = mock(); + const mockToastService = mock(); + const mockPlatformUtilsService = mock(); + const mockPasswordRepromptService = mock(); + const mockCipherService = mock(); + const mockMessagingService = mock(); + + beforeEach(() => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(mockCurrentTab); + + mockAutofillService.collectPageDetailsFromTab$.mockReturnValue(new BehaviorSubject([])); + + testBed = TestBed.configureTestingModule({ + providers: [ + { provide: AutofillService, useValue: mockAutofillService }, + { provide: I18nService, useValue: mockI18nService }, + { provide: ToastService, useValue: mockToastService }, + { provide: PlatformUtilsService, useValue: mockPlatformUtilsService }, + { provide: PasswordRepromptService, useValue: mockPasswordRepromptService }, + { provide: CipherService, useValue: mockCipherService }, + { provide: MessagingService, useValue: mockMessagingService }, + ], + }); + + service = testBed.inject(VaultPopupAutofillService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should be created", () => { + expect(service).toBeTruthy(); + }); + + describe("currentAutofillTab$", () => { + it("should return null if in popout", (done) => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + service.refreshCurrentTab(); + service.currentAutofillTab$.subscribe((tab) => { + expect(tab).toBeNull(); + done(); + }); + }); + + it("should return BrowserApi.getTabFromCurrentWindow() if not in popout", (done) => { + service.currentAutofillTab$.subscribe((tab) => { + expect(tab).toEqual(mockCurrentTab); + expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalled(); + done(); + }); + }); + + it("should only fetch the current tab once when subscribed to multiple times", async () => { + const firstTracked = subscribeTo(service.currentAutofillTab$); + const secondTracked = subscribeTo(service.currentAutofillTab$); + + await firstTracked.pauseUntilReceived(1); + await secondTracked.pauseUntilReceived(1); + + expect(BrowserApi.getTabFromCurrentWindow).toHaveBeenCalledTimes(1); + }); + }); + + describe("autofillAllowed$", () => { + it("should return true if there is a current tab", (done) => { + service.autofillAllowed$.subscribe((allowed) => { + expect(allowed).toBe(true); + done(); + }); + }); + + it("should return false if there is no current tab", (done) => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); + service.refreshCurrentTab(); + service.autofillAllowed$.subscribe((allowed) => { + expect(allowed).toBe(false); + done(); + }); + }); + + it("should return false if in a popout", (done) => { + jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + service.refreshCurrentTab(); + service.autofillAllowed$.subscribe((allowed) => { + expect(allowed).toBe(false); + done(); + }); + }); + }); + + describe("refreshCurrentTab()", () => { + it("should refresh currentAutofillTab$", async () => { + const tracked = subscribeTo(service.currentAutofillTab$); + service.refreshCurrentTab(); + await tracked.pauseUntilReceived(2); + }); + }); + + describe("autofill methods", () => { + const mockPageDetails: PageDetail[] = [{ tab: mockCurrentTab, details: {} as any, frameId: 1 }]; + let mockCipher: CipherView; + let expectedAutofillArgs: AutoFillOptions; + let mockPageDetails$: BehaviorSubject; + + beforeEach(() => { + mockCipher = new CipherView(); + mockCipher.type = CipherType.Login; + + mockPageDetails$ = new BehaviorSubject(mockPageDetails); + + mockAutofillService.collectPageDetailsFromTab$.mockReturnValue(mockPageDetails$); + + expectedAutofillArgs = { + tab: mockCurrentTab, + cipher: mockCipher, + pageDetails: mockPageDetails, + doc: expect.any(Document), + fillNewPassword: true, + allowTotpAutofill: true, + }; + + // Refresh the current tab so the mockedPageDetails$ are used + service.refreshCurrentTab(); + }); + + describe("doAutofill()", () => { + it("should return true if autofill is successful", async () => { + mockAutofillService.doAutoFill.mockResolvedValue(null); + const result = await service.doAutofill(mockCipher); + expect(result).toBe(true); + expect(mockAutofillService.doAutoFill).toHaveBeenCalledWith(expectedAutofillArgs); + }); + + it("should return false if autofill is not successful", async () => { + mockAutofillService.doAutoFill.mockRejectedValue(null); + const result = await service.doAutofill(mockCipher); + expect(result).toBe(false); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: mockI18nService.t("autofillError"), + }); + }); + + it("should return false if tab is null", async () => { + jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); + const result = await service.doAutofill(mockCipher); + expect(result).toBe(false); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: mockI18nService.t("autofillError"), + }); + }); + + it("should return false if missing page details", async () => { + mockPageDetails$.next([]); + const result = await service.doAutofill(mockCipher); + expect(result).toBe(false); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: mockI18nService.t("autofillError"), + }); + }); + + it("should show password prompt if cipher requires reprompt", async () => { + mockCipher.reprompt = CipherRepromptType.Password; + mockPasswordRepromptService.showPasswordPrompt.mockResolvedValue(false); + const result = await service.doAutofill(mockCipher); + expect(result).toBe(false); + }); + + it("should copy TOTP code to clipboard if available", async () => { + const totpCode = "123456"; + mockAutofillService.doAutoFill.mockResolvedValue(totpCode); + await service.doAutofill(mockCipher); + expect(mockPlatformUtilsService.copyToClipboard).toHaveBeenCalledWith( + totpCode, + expect.anything(), + ); + }); + + describe("closePopup", () => { + beforeEach(() => { + jest.spyOn(BrowserApi, "closePopup").mockImplementation(); + jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true); + mockPlatformUtilsService.isFirefox.mockReturnValue(true); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it("should close popup by default when in popup", async () => { + await service.doAutofill(mockCipher); + expect(BrowserApi.closePopup).toHaveBeenCalled(); + }); + + it("should not close popup when closePopup is set to false", async () => { + await service.doAutofill(mockCipher, false); + expect(BrowserApi.closePopup).not.toHaveBeenCalled(); + }); + + it("should close popup after a timeout for chromium browsers", async () => { + mockPlatformUtilsService.isFirefox.mockReturnValue(false); + jest.spyOn(global, "setTimeout"); + await service.doAutofill(mockCipher); + jest.advanceTimersByTime(50); + expect(setTimeout).toHaveBeenCalledTimes(1); + expect(BrowserApi.closePopup).toHaveBeenCalled(); + }); + }); + }); + + describe("doAutofillAndSave()", () => { + beforeEach(() => { + // Mocks for service._closePopup() + jest.spyOn(BrowserApi, "closePopup").mockImplementation(); + jest.spyOn(BrowserPopupUtils, "inPopup").mockReturnValue(true); + mockPlatformUtilsService.isFirefox.mockReturnValue(true); + + // Default to happy path + mockAutofillService.doAutoFill.mockResolvedValue(null); + mockCipherService.updateWithServer.mockResolvedValue(null); + }); + + it("should return false if cipher is not login type", async () => { + mockCipher.type = CipherType.Card; + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(false); + expect(mockAutofillService.doAutoFill).not.toHaveBeenCalled(); + }); + + it("should return false if autofill is not successful", async () => { + mockAutofillService.doAutoFill.mockRejectedValue(null); + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(false); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: mockI18nService.t("autofillError"), + }); + }); + + it("should return true if the cipher already has a URI for the tab", async () => { + mockCipher.login = new LoginView(); + mockCipher.login.uris = [{ uri: mockCurrentTab.url } as LoginUriView]; + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(true); + expect(BrowserApi.closePopup).toHaveBeenCalled(); + expect(mockCipherService.updateWithServer).not.toHaveBeenCalled(); + }); + + it("should show a success toast if closePopup is false and cipher already has URI for tab", async () => { + mockCipher.login = new LoginView(); + mockCipher.login.uris = [{ uri: mockCurrentTab.url } as LoginUriView]; + const result = await service.doAutofillAndSave(mockCipher, false); + expect(result).toBe(true); + expect(BrowserApi.closePopup).not.toHaveBeenCalled(); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: mockI18nService.t("autoFillSuccessAndSavedUri"), + }); + expect(mockCipherService.updateWithServer).not.toHaveBeenCalled(); + }); + + it("should add a URI to the cipher and save with the server", async () => { + const mockEncryptedCipher = {} as Cipher; + mockCipherService.encrypt.mockResolvedValue(mockEncryptedCipher); + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(true); + expect(mockCipher.login.uris).toHaveLength(1); + expect(mockCipher.login.uris[0].uri).toBe(mockCurrentTab.url); + expect(mockCipherService.encrypt).toHaveBeenCalledWith(mockCipher); + expect(mockCipherService.updateWithServer).toHaveBeenCalledWith(mockEncryptedCipher); + }); + + it("should add a URI to the cipher when there are no existing URIs", async () => { + mockCipher.login.uris = null; + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(true); + expect(mockCipher.login.uris).toHaveLength(1); + expect(mockCipher.login.uris[0].uri).toBe(mockCurrentTab.url); + }); + + it("should show an error toast if saving the cipher fails", async () => { + mockCipherService.updateWithServer.mockRejectedValue(null); + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(false); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: null, + message: mockI18nService.t("unexpectedError"), + }); + }); + + it("should close the popup after saving the cipher", async () => { + const result = await service.doAutofillAndSave(mockCipher); + expect(result).toBe(true); + expect(BrowserApi.closePopup).toHaveBeenCalled(); + }); + + it("should show success toast after saving the cipher if closePop is false", async () => { + mockAutofillService.doAutoFill.mockResolvedValue(null); + const result = await service.doAutofillAndSave(mockCipher, false); + expect(result).toBe(true); + expect(BrowserApi.closePopup).not.toHaveBeenCalled(); + expect(mockToastService.showToast).toHaveBeenCalledWith({ + variant: "success", + title: null, + message: mockI18nService.t("autoFillSuccessAndSavedUri"), + }); + }); + }); + }); +}); diff --git a/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts new file mode 100644 index 00000000000..ca59ffd9979 --- /dev/null +++ b/apps/browser/src/vault/popup/services/vault-popup-autofill.service.ts @@ -0,0 +1,237 @@ +import { Injectable } from "@angular/core"; +import { + firstValueFrom, + map, + Observable, + of, + shareReplay, + startWith, + Subject, + switchMap, +} from "rxjs"; + +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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; +import { CipherRepromptType, CipherType } from "@bitwarden/common/vault/enums"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; +import { ToastService } from "@bitwarden/components"; +import { PasswordRepromptService } from "@bitwarden/vault"; + +import { + AutofillService, + PageDetail, +} from "../../../autofill/services/abstractions/autofill.service"; +import { BrowserApi } from "../../../platform/browser/browser-api"; +import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; + +@Injectable({ + providedIn: "root", +}) +export class VaultPopupAutofillService { + private _refreshCurrentTab$ = new Subject(); + + /** + * Observable that contains the current tab to be considered for autofill. If there is no current tab + * or the popup is in a popout window, this will be null. + */ + currentAutofillTab$: Observable = this._refreshCurrentTab$.pipe( + startWith(null), + switchMap(async () => { + if (BrowserPopupUtils.inPopout(window)) { + return null; + } + return await BrowserApi.getTabFromCurrentWindow(); + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + /** + * Observable that indicates whether autofill is allowed in the current context. + * Autofill is allowed when there is a current tab and the popup is not in a popout window. + */ + autofillAllowed$: Observable = this.currentAutofillTab$.pipe(map((tab) => !!tab)); + + private _currentPageDetails$: Observable = this.currentAutofillTab$.pipe( + switchMap((tab) => { + if (!tab) { + return of([]); + } + return this.autofillService.collectPageDetailsFromTab$(tab); + }), + shareReplay({ refCount: false, bufferSize: 1 }), + ); + + constructor( + private autofillService: AutofillService, + private i18nService: I18nService, + private toastService: ToastService, + private platformUtilService: PlatformUtilsService, + private passwordRepromptService: PasswordRepromptService, + private cipherService: CipherService, + private messagingService: MessagingService, + ) { + this._currentPageDetails$.subscribe(); + } + + private async _internalDoAutofill( + cipher: CipherView, + tab: chrome.tabs.Tab, + pageDetails: PageDetail[], + ): Promise { + if ( + cipher.reprompt !== CipherRepromptType.None && + !(await this.passwordRepromptService.showPasswordPrompt()) + ) { + return false; + } + + if (tab == null || pageDetails.length === 0) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("autofillError"), + }); + return false; + } + + try { + const totpCode = await this.autofillService.doAutoFill({ + tab, + cipher, + pageDetails, + doc: window.document, + fillNewPassword: true, + allowTotpAutofill: true, + }); + + if (totpCode != null) { + this.platformUtilService.copyToClipboard(totpCode, { window: window }); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("autofillError"), + }); + return false; + } + + return true; + } + + private _closePopup() { + if (!BrowserPopupUtils.inPopup(window)) { + return; + } + + if (this.platformUtilService.isFirefox() || this.platformUtilService.isSafari()) { + BrowserApi.closePopup(window); + return; + } + + // Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard + setTimeout(() => BrowserApi.closePopup(window), 50); + } + + /** + * Re-fetch the current tab + */ + refreshCurrentTab() { + this._refreshCurrentTab$.next(null); + } + + /** + * Attempts to autofill the given cipher. Returns true if the autofill was successful, false otherwise. + * Will copy any TOTP code to the clipboard if available after successful autofill. + * @param cipher + * @param closePopup If true, will close the popup window after successful autofill. Defaults to true. + */ + async doAutofill(cipher: CipherView, closePopup = true): Promise { + const tab = await firstValueFrom(this.currentAutofillTab$); + const pageDetails = await firstValueFrom(this._currentPageDetails$); + + const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails); + + if (didAutofill && closePopup) { + this._closePopup(); + } + + return didAutofill; + } + + /** + * Attempts to autofill the given cipher and, upon successful autofill, saves the URI to the cipher. + * Will copy any TOTP code to the clipboard if available after successful autofill. + * @param cipher The cipher to autofill and save. Only Login ciphers are supported. + * @param closePopup If true, will close the popup window after successful autofill. + * If false, will show a success toast instead. Defaults to true. + */ + async doAutofillAndSave(cipher: CipherView, closePopup = true): Promise { + // We can only save URIs for login ciphers + if (cipher.type !== CipherType.Login) { + return false; + } + + const pageDetails = await firstValueFrom(this._currentPageDetails$); + const tab = await firstValueFrom(this.currentAutofillTab$); + + const didAutofill = await this._internalDoAutofill(cipher, tab, pageDetails); + + if (!didAutofill) { + return false; + } + + const didSaveUri = await this._saveNewUri(cipher, tab); + + if (!didSaveUri) { + return false; + } + + if (closePopup) { + this._closePopup(); + } else { + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("autoFillSuccessAndSavedUri"), + }); + } + + return true; + } + + /** + * Saves the current tab's URL as a new URI for the given cipher. If the cipher already has a URI for the tab, + * this method does nothing and returns true. + * @private + */ + private async _saveNewUri(cipher: CipherView, tab: chrome.tabs.Tab): Promise { + cipher.login.uris ??= []; + + if (cipher.login.uris.some((uri) => uri.uri === tab.url)) { + // Cipher already has a URI for this tab + return true; + } + + const loginUri = new LoginUriView(); + loginUri.uri = tab.url; + cipher.login.uris.push(loginUri); + + try { + const encCipher = await this.cipherService.encrypt(cipher); + await this.cipherService.updateWithServer(encCipher); + this.messagingService.send("editedCipher"); + return true; + } catch { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("unexpectedError"), + }); + return false; + } + } +} diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 0b40b136ab9..f6457b8db54 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -16,8 +16,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BrowserApi } from "../../../platform/browser/browser-api"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; +import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; import { VaultPopupItemsService } from "./vault-popup-items.service"; import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; @@ -36,6 +36,7 @@ describe("VaultPopupItemsService", () => { const vaultPopupListFiltersServiceMock = mock(); const searchService = mock(); const collectionService = mock(); + const vaultAutofillServiceMock = mock(); beforeEach(() => { allCiphers = cipherFactory(10); @@ -70,10 +71,10 @@ describe("VaultPopupItemsService", () => { vaultPopupListFiltersServiceMock.filterFunction$ = new BehaviorSubject( (ciphers: CipherView[]) => ciphers, ); - jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(false); - jest - .spyOn(BrowserApi, "getTabFromCurrentWindow") - .mockResolvedValue({ url: "https://example.com" } as chrome.tabs.Tab); + + vaultAutofillServiceMock.currentAutofillTab$ = new BehaviorSubject({ + url: "https://example.com", + } as chrome.tabs.Tab); mockOrg = { id: "org1", @@ -97,6 +98,7 @@ describe("VaultPopupItemsService", () => { { provide: OrganizationService, useValue: organizationServiceMock }, { provide: VaultPopupListFiltersService, useValue: vaultPopupListFiltersServiceMock }, { provide: CollectionService, useValue: collectionService }, + { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, ], }); @@ -155,15 +157,7 @@ describe("VaultPopupItemsService", () => { describe("autoFillCiphers$", () => { it("should return empty array if there is no current tab", (done) => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); - service.autoFillCiphers$.subscribe((ciphers) => { - expect(ciphers).toEqual([]); - done(); - }); - }); - - it("should return empty array if in Popout window", (done) => { - jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); + (vaultAutofillServiceMock.currentAutofillTab$ as BehaviorSubject).next(null); service.autoFillCiphers$.subscribe((ciphers) => { expect(ciphers).toEqual([]); done(); @@ -317,28 +311,16 @@ describe("VaultPopupItemsService", () => { done(); }); }); - }); - describe("autoFillAllowed$", () => { - it("should return true if there is a current tab", (done) => { - service.autofillAllowed$.subscribe((allowed) => { - expect(allowed).toBe(true); - done(); - }); - }); + it("should return true when all ciphers are deleted", (done) => { + cipherServiceMock.getAllDecrypted.mockResolvedValue([ + { id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true }, + { id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true }, + { id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true }, + ] as CipherView[]); - it("should return false if there is no current tab", (done) => { - jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); - service.autofillAllowed$.subscribe((allowed) => { - expect(allowed).toBe(false); - done(); - }); - }); - - it("should return false if in a Popout", (done) => { - jest.spyOn(BrowserPopupUtils, "inPopout").mockReturnValue(true); - service.autofillAllowed$.subscribe((allowed) => { - expect(allowed).toBe(false); + service.emptyVault$.subscribe((empty) => { + expect(empty).toBe(true); done(); }); }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index c6d155c5219..eb1af2cf713 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -28,11 +28,10 @@ import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { BrowserApi } from "../../../platform/browser/browser-api"; import { runInsideAngular } from "../../../platform/browser/run-inside-angular.operator"; -import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import { PopupCipherView } from "../views/popup-cipher.view"; +import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; /** @@ -42,7 +41,6 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi providedIn: "root", }) export class VaultPopupItemsService { - private _refreshCurrentTab$ = new Subject(); private _searchText$ = new BehaviorSubject(""); /** @@ -70,22 +68,6 @@ export class VaultPopupItemsService { }), ); - /** - * Observable that contains the current tab to be considered for autofill. If there is no current tab - * or the popup is in a popout window, this will be null. - * @private - */ - private _currentAutofillTab$: Observable = this._refreshCurrentTab$.pipe( - startWith(null), - switchMap(async () => { - if (BrowserPopupUtils.inPopout(window)) { - return null; - } - return await BrowserApi.getTabFromCurrentWindow(); - }), - shareReplay({ refCount: false, bufferSize: 1 }), - ); - /** * Observable that contains the list of all decrypted ciphers. * @private @@ -105,14 +87,16 @@ export class VaultPopupItemsService { map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col])); - return ciphers.map( - (cipher) => - new PopupCipherView( - cipher, - cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), - orgMap[cipher.organizationId as OrganizationId], - ), - ); + return ciphers + .filter((c) => !c.isDeleted) + .map( + (cipher) => + new PopupCipherView( + cipher, + cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]), + orgMap[cipher.organizationId as OrganizationId], + ), + ); }), ), ), @@ -145,7 +129,7 @@ export class VaultPopupItemsService { autoFillCiphers$: Observable = combineLatest([ this._filteredCipherList$, this._otherAutoFillTypes$, - this._currentAutofillTab$, + this.vaultPopupAutofillService.currentAutofillTab$, ]).pipe( switchMap(([ciphers, otherTypes, tab]) => { if (!tab) { @@ -217,12 +201,6 @@ export class VaultPopupItemsService { }), ); - /** - * Observable that indicates whether autofill is allowed in the current context. - * Autofill is allowed when there is a current tab and the popup is not in a popout window. - */ - autofillAllowed$: Observable = this._currentAutofillTab$.pipe(map((tab) => !!tab)); - /** * Observable that indicates whether the user's vault is empty. */ @@ -257,15 +235,9 @@ export class VaultPopupItemsService { private organizationService: OrganizationService, private searchService: SearchService, private collectionService: CollectionService, + private vaultPopupAutofillService: VaultPopupAutofillService, ) {} - /** - * Re-fetch the current tab to trigger a re-evaluation of the autofill ciphers. - */ - refreshCurrentTab() { - this._refreshCurrentTab$.next(null); - } - applyFilter(newSearchText: string) { this._searchText$.next(newSearchText); } diff --git a/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts b/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts index acee6ba20f7..2e715c15af0 100644 --- a/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts +++ b/apps/browser/src/vault/services/fido2-user-verification.service.spec.ts @@ -89,7 +89,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -105,7 +105,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -122,7 +122,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -135,7 +135,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -198,7 +198,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -214,7 +214,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); @@ -231,7 +231,7 @@ describe("Fido2UserVerificationService", () => { ); expect(UserVerificationDialogComponent.open).toHaveBeenCalledWith(dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); expect(result).toBe(true); }); diff --git a/apps/browser/src/vault/services/fido2-user-verification.service.ts b/apps/browser/src/vault/services/fido2-user-verification.service.ts index 90c4d8ca610..3c91001fd56 100644 --- a/apps/browser/src/vault/services/fido2-user-verification.service.ts +++ b/apps/browser/src/vault/services/fido2-user-verification.service.ts @@ -54,7 +54,7 @@ export class Fido2UserVerificationService { private async showUserVerificationDialog(): Promise { const result = await UserVerificationDialogComponent.open(this.dialogService, { - clientSideOnlyVerification: true, + verificationType: "client", }); if (result.userAction === "cancel") { diff --git a/apps/browser/store/locales/lv/copy.resx b/apps/browser/store/locales/lv/copy.resx index 6d75e8ff0ef..b45743f2e4c 100644 --- a/apps/browser/store/locales/lv/copy.resx +++ b/apps/browser/store/locales/lv/copy.resx @@ -124,48 +124,48 @@ Bitwarden viegli aizsargā visas paroles, paroļu atslēgas un jutīgu informāciju mājās, darbā vai ceļā. - Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more! + PCMag, WIRED, The Verge, CNET, G2 un vēl ir atzinuši Bitwarden kā 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. +AIZSARGĀ SAVU DIGITĀLO DZĪVI +Aizsargā savu digitālo dzīvi un nodrošinies pret datu noplūdēm ar vienreizēju, spēcīgu paruļu izveidošanu un saglabāšanu katram kontam! Glabā visu pilnīgi šifrētā paroļu glabātavā, kurai vari piekļūt vienīgi Tu! -ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE -Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions. +PIEKĻŪSTI SAVIEM DATIEM JEBKUR, JEBKAD UN JEBKURĀ IERĪCĒ +Viegla paroļu pārvaldīšana, glabāšana, nodrošināšana un kopīgošana bez ierobežojumiem dažādās ierīcēs. -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. +IKVIENAM VAJADZĒTU IZMANTOT RĪKUS, LAI TIEŠSAISTĒ BŪTU DROŠI +Bitwarden ir izmantojams bez maksas, reklāmām un datu pārdošanas. Bitwarden tic, ka ikvienam vajadzētu būt iespējai būt drošam tiešsaistē. Premium plāni sniedz piekļuvi papildu iespējām. -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. +SPĒCINI SAVAS KOMANDAS AR BITWARDEN +Komandu un uzņēmumu plāni satur profesionālas uzņēmējdarbības iespējas. Piemēram, SSO iekļaušana, pašmitināšana, direktorijas iekļaušana un SCIM apgāde, visaptveroši nosacījumi, API piekļuve, notikumu žurnāli un vēl. -Use Bitwarden to secure your workforce and share sensitive information with colleagues. +Izmanto bitwarden, lai drošinātu savu darbaspēku un kopīgotu jutīgu informāciju ar darbabiedriem! -More reasons to choose Bitwarden: +Vairāk iemelsu, lai izvēlētos 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. +Pasaules klases šifrēšana +Paroles ir aizsargātas ar sarežģītu pilnīgu šifrēšanu (AES-256 bitu, sālīts jaucējkods un PBKDF2 SHA-256), līdz ar to Tavi dati ir droši un privāti. -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. +Trešo pušu pārbaudes +Bitwarden pastāvīgi veic aptverošas trešo pušu drošības pārbaudes ar atzīstamiem drošības uzņēmumiem. Šīs ikgadējās pārbaudes iekļauj pirmkoda izvērtēšanu un ielaušanās pārbaudes dažādās Bitwarden IP adresēs, serveros un tīmekļa lietotnē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. +Papildu 2FA +Aizsargā savu pieteikšanos ar trešo pušu autentificētāju, e-pastā nosūtītiem kodiem vai tādiem FIDO2 WebAuthn pieteikšanās datiem kā aparatūras drošības atslēgu vai piekļuves atslēgu! Bitwarden Send -Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure. +Datu pārsūtīšana citiem, saglabājot pilnīgu šifrēšanas drošību un ierobežojot atklāšanu. -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. +Iebūvēts veidotājs +Izveido garas, sarežģītas un atšķirīgas paroles un vienreizējus lietotājvārdus katrai apmeklētajai vietnei! Papildu privātumam var apvienot ar e-pasta aizstājvārdu nodrošinātājiem. -Global Translations -Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin. +Vispasaules tulkojumi +Bitwarden ir tulkots vairāk nekā 60 valodās ar vispasaules kopienas palīdzību tulkošanas rīkā Crowdin. -Cross-Platform Applications -Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more. +Starpplatformu lietotnes +Aizsargā un kopīgo jutīgus datus savā Bitwarden glabātavā jebkurā pārlūkā, mobilajā ierīcē vai darbvirsmas operētājsistēmā un vēl! -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 aizsargā vairāk nekā tikai paroles +Pilnīgas šifrēšanas pieteikšanās datu pārvaldības risinājumi no Bitwarden sniedz iespēju apvienībām aizsargāt visu, tajā skaitā izstrādātāju noslēpumus un piekļuves atslēgu pieredzes. Bitwarden.com ir ampeklējams, lai uzzinātu vairāk par Bitwarden noslēpumu pārvaldnieku un Bitwarden Passwordless.dev. diff --git a/apps/cli/README.md b/apps/cli/README.md index b2d87ebaf91..e00b9a345d2 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -41,6 +41,9 @@ We provide natively packaged versions of the CLI for each platform which have no ```bash brew install bitwarden-cli ``` + > ⚠️ The homebrew version is not recommended for all users. + > + > Homebrew pulls the CLI's GPL build and does not include device approval commands for Enterprise SSO customers. - [Snap](https://snapcraft.io/bw) ```bash sudo snap install bw diff --git a/apps/cli/package.json b/apps/cli/package.json index 97b4dcf2ecf..2822bd52ca7 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.6.0", + "version": "2024.7.0", "keywords": [ "bitwarden", "password", @@ -15,7 +15,7 @@ "type": "git", "url": "https://github.com/bitwarden/clients" }, - "license": "GPL-3.0-only", + "license": "SEE LICENSE IN LICENSE.txt", "scripts": { "clean": "rimraf dist", "build:oss": "webpack", diff --git a/apps/cli/src/admin-console/.eslintrc.json b/apps/cli/src/admin-console/.eslintrc.json new file mode 100644 index 00000000000..38467187294 --- /dev/null +++ b/apps/cli/src/admin-console/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../libs/admin-console/.eslintrc.json" +} diff --git a/apps/cli/src/auth/commands/login.command.ts b/apps/cli/src/auth/commands/login.command.ts index 5e80f6faf40..3b67f955406 100644 --- a/apps/cli/src/auth/commands/login.command.ts +++ b/apps/cli/src/auth/commands/login.command.ts @@ -35,9 +35,9 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { Utils } from "@bitwarden/common/platform/misc/utils"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { NodeUtils } from "@bitwarden/node/node-utils"; import { Response } from "../../models/response"; diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 536c9e3b8c2..88574635e1c 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -128,17 +128,7 @@ export class ListCommand { ciphers = this.searchService.searchCiphersBasic(ciphers, options.search, options.trash); } - for (let i = 0; i < ciphers.length; i++) { - const c = ciphers[i]; - // Set upload immediately on the last item in the ciphers collection to avoid the event collection - // service from uploading each time. - await this.eventCollectionService.collect( - EventType.Cipher_ClientViewed, - c.id, - i === ciphers.length - 1, - c.organizationId, - ); - } + await this.eventCollectionService.collectMany(EventType.Cipher_ClientViewed, ciphers, true); const res = new ListResponse(ciphers.map((o) => new CipherResponse(o))); return Response.success(res); diff --git a/apps/cli/src/locales/en/messages.json b/apps/cli/src/locales/en/messages.json index 3f753781127..f8a49c6e923 100644 --- a/apps/cli/src/locales/en/messages.json +++ b/apps/cli/src/locales/en/messages.json @@ -5,8 +5,8 @@ "authenticatorAppTitle": { "message": "Authenticator App" }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP Security Key" }, "emailTitle": { "message": "Email" diff --git a/apps/cli/src/service-container.ts b/apps/cli/src/service-container.ts index 2d5f83787f1..aeb233a31da 100644 --- a/apps/cli/src/service-container.ts +++ b/apps/cli/src/service-container.ts @@ -106,8 +106,6 @@ import { EventUploadService } from "@bitwarden/common/services/event/event-uploa import { SearchService } from "@bitwarden/common/services/search.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service"; -import { legacyPasswordGenerationServiceFactory } from "@bitwarden/common/tools/generator"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, @@ -124,6 +122,10 @@ import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-u import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service"; import { TotpService } from "@bitwarden/common/vault/services/totp.service"; +import { + legacyPasswordGenerationServiceFactory, + PasswordGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; import { ImportApiService, ImportApiServiceAbstraction, @@ -729,6 +731,7 @@ export class ServiceContainer { this.organizationService, this.eventUploadService, this.authService, + this.accountService, ); this.providerApiService = new ProviderApiService(this.apiService); diff --git a/apps/cli/src/tools/generate.command.ts b/apps/cli/src/tools/generate.command.ts index 2bbfb02267b..2829bc51414 100644 --- a/apps/cli/src/tools/generate.command.ts +++ b/apps/cli/src/tools/generate.command.ts @@ -1,10 +1,12 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { DefaultPassphraseGenerationOptions } from "@bitwarden/common/tools/generator/passphrase"; import { DefaultPasswordGenerationOptions, + DefaultPassphraseGenerationOptions, +} from "@bitwarden/generator-core"; +import { + PasswordGeneratorOptions, PasswordGenerationServiceAbstraction, -} from "@bitwarden/common/tools/generator/password"; -import { PasswordGeneratorOptions } from "@bitwarden/common/tools/generator/password/password-generator-options"; +} from "@bitwarden/generator-legacy"; import { Response } from "../models/response"; import { StringResponse } from "../models/response/string.response"; diff --git a/apps/cli/stores/snap/snapcraft.yaml b/apps/cli/stores/snap/snapcraft.yaml index 865adcfe4b8..c79d8fc399c 100644 --- a/apps/cli/stores/snap/snapcraft.yaml +++ b/apps/cli/stores/snap/snapcraft.yaml @@ -1,6 +1,7 @@ name: bw version: __version__ summary: Bitwarden CLI - A secure and free password manager for all of your devices. +license: Proprietary description: | Bitwarden, Inc. is the parent company of 8bit Solutions LLC. diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index b5e9b5a1900..0a34b05496f 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -17,6 +17,10 @@ "@bitwarden/auth/angular": ["../../libs/auth/src/angular"], "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/generator-core": ["../../libs/tools/generator/core/src"], + "@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"], + "@bitwarden/generator-history": ["../../libs/tools/generator/extensions/history/src"], + "@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"], "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], diff --git a/apps/desktop/config/base.json b/apps/desktop/config/base.json index 2c7b4dcae16..7f18c63878b 100644 --- a/apps/desktop/config/base.json +++ b/apps/desktop/config/base.json @@ -1,7 +1,6 @@ { "devFlags": {}, "flags": { - "multithreadDecryption": false, "enableCipherKeyEncryption": true } } diff --git a/apps/desktop/desktop_native/.cargo/config b/apps/desktop/desktop_native/.cargo/config.toml similarity index 100% rename from apps/desktop/desktop_native/.cargo/config rename to apps/desktop/desktop_native/.cargo/config.toml diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 8617553b377..0b6dc631049 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -45,32 +45,31 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arboard" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" +checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2", + "objc2-app-kit", + "objc2-foundation", "parking_lot", - "thiserror", "wl-clipboard-rs", "x11rb", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -87,24 +86,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - [[package]] name = "block-buffer" version = "0.10.4" @@ -123,6 +110,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2", +] + [[package]] name = "cbc" version = "0.1.2" @@ -134,15 +130,15 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.90" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" [[package]] name = "cfg-expr" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon", @@ -172,9 +168,9 @@ dependencies = [ [[package]] name = "clipboard-win" -version = "5.3.0" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" dependencies = [ "error-code", ] @@ -235,9 +231,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", "syn", @@ -245,9 +241,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.120" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dc7287237dd438b926a81a1a5605dad33d286870e5eee2db17bf2bcd9e92a" +checksum = "273dcfd3acd4e1e276af13ed2a43eea7001318823e7a726a6b3ed39b4acc0b82" dependencies = [ "cc", "cxxbridge-flags", @@ -257,9 +253,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.120" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47c6c8ad7c1a10d3ef0fe3ff6733f4db0d78f08ef0b13121543163ef327058b" +checksum = "d8b2766fbd92be34e9ed143898fce6c572dc009de39506ed6903e5a05b68914e" dependencies = [ "cc", "codespan-reporting", @@ -272,15 +268,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.120" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "701a1ac7a697e249cdd8dc026d7a7dafbfd0dbcd8bd24ec55889f2bc13dd6287" +checksum = "839fcd5e43464614ffaa989eaf1c139ef1f0c51672a1ed08023307fa1b909ccd" [[package]] name = "cxxbridge-macro" -version = "1.0.120" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b404f596046b0bb2d903a9c786b875a126261b52b7c3a64bbb66382c41c771df" +checksum = "4b2c1c1776b986979be68bb2285da855f8d8a35851a769fca8740df7c3d07877" dependencies = [ "proc-macro2", "quote", @@ -299,7 +295,7 @@ dependencies = [ ] [[package]] -name = "desktop_native" +name = "desktop_core" version = "0.0.0" dependencies = [ "aes", @@ -311,9 +307,6 @@ dependencies = [ "gio", "keytar", "libsecret", - "napi", - "napi-build", - "napi-derive", "rand", "retry", "scopeguard", @@ -326,6 +319,17 @@ dependencies = [ "windows", ] +[[package]] +name = "desktop_napi" +version = "0.0.0" +dependencies = [ + "anyhow", + "desktop_core", + "napi", + "napi-build", + "napi-derive", +] + [[package]] name = "digest" version = "0.10.7" @@ -347,9 +351,9 @@ dependencies = [ [[package]] name = "downcast-rs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "equivalent" @@ -359,9 +363,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", @@ -375,9 +379,9 @@ checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fixedbitset" @@ -476,9 +480,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -487,15 +491,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gio" -version = "0.19.2" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eae10b27b6dd27e22ed0d812c6387deba295e6fc004a8b379e459b663b05a02" +checksum = "be548be810e45dd31d3bbb89c6210980bb7af9bca3ea1292b5f16b75f8e394a7" dependencies = [ "futures-channel", "futures-core", @@ -511,9 +515,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.19.0" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4" +checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef" dependencies = [ "glib-sys", "gobject-sys", @@ -524,11 +528,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.19.3" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e191cc1af1f35b9699213107068cd3fe05d9816275ac118dc785a0dd8faebf" +checksum = "b664491bc77ab55daa6714a592cdbe1a55e28abec09cb50e87689b90de456ff4" dependencies = [ - "bitflags 2.5.0", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -546,9 +550,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.3" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9972bb91643d589c889654693a4f1d07697fdcb5d104b5c44fb68649ba1bf68d" +checksum = "1d405205a405182f95e637710850a8e82f25ba01fdd6baebc82dabeaf0883376" dependencies = [ "heck", "proc-macro-crate", @@ -559,9 +563,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.19.0" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4" +checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5" dependencies = [ "libc", "system-deps", @@ -569,9 +573,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.19.0" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979" +checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e" dependencies = [ "glib-sys", "libc", @@ -580,9 +584,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "heck" @@ -648,9 +652,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -659,7 +663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -699,15 +703,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -719,20 +723,11 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "minimal-lexical" @@ -742,20 +737,20 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] [[package]] name = "napi" -version = "2.16.0" +version = "2.16.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" +checksum = "dfc300228808a0e6aea5a58115c82889240bcf8dab16fc25ad675b33e454b368" dependencies = [ - "bitflags 2.5.0", + "bitflags", "ctor", "napi-derive", "napi-sys", @@ -765,15 +760,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" +checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a" [[package]] name = "napi-derive" -version = "2.16.0" +version = "2.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce" +checksum = "e0e034ddf6155192cf83f267ede763fe6c164dfa9971585436b16173718d94c4" dependencies = [ "cfg-if", "convert_case", @@ -785,9 +780,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "1.0.63" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5126b64f6ad9e28e30e6d15213dd378626b38f556454afebc42f7f02a90902" +checksum = "bff2c00437f3b3266391eb5e6aa25d0029187daf5caf05b8e3271468fb5ae73e" dependencies = [ "convert_case", "once_cell", @@ -800,9 +795,9 @@ dependencies = [ [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +checksum = "427802e8ec3a734331fec1035594a210ce1ff4dc5bc1950530920ab717964ea3" dependencies = [ "libloading", ] @@ -813,7 +808,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.5.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -840,39 +835,109 @@ dependencies = [ ] [[package]] -name = "objc" -version = "0.2.7" +name = "objc-sys" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "malloc_buf", + "objc-sys", + "objc2-encode", ] [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "objc2-app-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "block", - "objc", - "objc_id", + "bitflags", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] -name = "objc_id" -version = "0.1.1" +name = "objc2-core-data" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "objc", + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", +] + +[[package]] +name = "objc2-encode" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891e71393cd1f227313c9379a26a584ff3d7e6e7159e988851f0934c993f0f8" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] name = "object" -version = "0.32.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -885,9 +950,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "os_pipe" -version = "1.1.5" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +checksum = "29d73ba8daf8fac13b0501d1abeddcfe21ba7401ada61a819144b6c2a4f32209" dependencies = [ "libc", "windows-sys", @@ -895,9 +960,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -905,22 +970,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap", @@ -961,9 +1026,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -979,9 +1044,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -1018,18 +1083,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 1.3.2", + "bitflags", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -1039,9 +1104,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -1050,9 +1115,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "retry" @@ -1065,17 +1130,17 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -1102,11 +1167,11 @@ checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -1115,9 +1180,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -1125,24 +1190,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -1151,9 +1216,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -1186,9 +1251,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "syn" -version = "2.0.58" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -1237,18 +1302,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -1257,9 +1322,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "num_cpus", @@ -1268,21 +1333,21 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.9", + "toml_edit 0.22.14", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -1300,22 +1365,22 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.9" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.5", + "winnow 0.6.13", ] [[package]] name = "tree_magic_mini" -version = "3.1.4" +version = "3.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" +checksum = "469a727cac55b41448315cc10427c069c618ac59bb6a4480283fcd811749bdc2" dependencies = [ "fnv", "home", @@ -1345,9 +1410,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "version-compare" @@ -1369,9 +1434,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wayland-backend" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +checksum = "34e9e6b6d4a2bb4e7e69433e0b35c7923b95d4dc8503a84d25ec917a4bbfdf07" dependencies = [ "cc", "downcast-rs", @@ -1383,11 +1448,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +checksum = "1e63801c85358a431f986cffa74ba9599ff571fc5774ac113ed3b490c19a1133" dependencies = [ - "bitflags 2.5.0", + "bitflags", "rustix", "wayland-backend", "wayland-scanner", @@ -1399,7 +1464,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.5.0", + "bitflags", "wayland-backend", "wayland-client", "wayland-scanner", @@ -1411,7 +1476,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.5.0", + "bitflags", "wayland-backend", "wayland-client", "wayland-protocols", @@ -1420,9 +1485,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +checksum = "67da50b9f80159dec0ea4c11c13e24ef9e7574bd6ce24b01860a175010cea565" dependencies = [ "proc-macro2", "quick-xml", @@ -1431,9 +1496,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +checksum = "105b1842da6554f91526c14a2a2172897b7f745a805d62af4ce698706be79c12" dependencies = [ "dlib", "log", @@ -1442,68 +1507,70 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" -version = "0.54.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" dependencies = [ "windows-core", - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] name = "windows-core" -version = "0.54.0" +version = "0.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" dependencies = [ + "windows-implement", + "windows-interface", "windows-result", - "windows-targets 0.52.4", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "windows-result" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1512,7 +1579,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.5", ] [[package]] @@ -1532,17 +1599,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -1553,9 +1621,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -1565,9 +1633,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -1577,9 +1645,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -1589,9 +1663,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -1601,9 +1675,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -1613,9 +1687,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -1625,9 +1699,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -1640,9 +1714,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] @@ -1669,9 +1743,9 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "gethostname", "rustix", @@ -1680,6 +1754,6 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index cded3d57ef6..c6b77473b2a 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -1,57 +1,3 @@ -[package] -edition = "2021" -exclude = ["index.node"] -license = "GPL-3.0" -name = "desktop_native" -version = "0.0.0" - -[lib] -crate-type = ["cdylib"] - -[features] -default = [] -manual_test = [] - -[dependencies] -aes = "=0.8.4" -anyhow = "=1.0.86" -arboard = { version = "=3.3.2", default-features = false, features = ["wayland-data-control"] } -base64 = "=0.22.1" -cbc = { version = "=0.1.2", features = ["alloc"] } -napi = { version = "=2.16.0", features = ["async"] } -napi-derive = "=2.16.0" -rand = "=0.8.5" -retry = "=2.0.0" -scopeguard = "=1.2.0" -sha2 = "=0.10.8" -thiserror = "=1.0.58" -typenum = "=1.17.0" - -[build-dependencies] -napi-build = "=2.1.2" - -[target.'cfg(windows)'.dependencies] -widestring = "=1.0.2" -windows = { version = "=0.54.0", features = [ - "Foundation", - "Security_Credentials_UI", - "Security_Cryptography", - "Storage_Streams", - "Win32_Foundation", - "Win32_Security_Credentials", - "Win32_System_WinRT", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_WindowsAndMessaging", -] } - -[target.'cfg(windows)'.dev-dependencies] -keytar = "=0.1.6" - -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation = "=0.9.4" -security-framework = "=2.9.2" -security-framework-sys = "=2.9.1" - -[target.'cfg(target_os = "linux")'.dependencies] -gio = "=0.19.2" -libsecret = "=0.5.0" +[workspace] +resolver = "2" +members = ["napi", "core"] diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml new file mode 100644 index 00000000000..1beb2ee08ce --- /dev/null +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -0,0 +1,51 @@ +[package] +edition = "2021" +license = "GPL-3.0" +name = "desktop_core" +version = "0.0.0" +publish = false + +[features] +default = [] +manual_test = [] + +[dependencies] +aes = "=0.8.4" +anyhow = "=1.0.86" +arboard = { version = "=3.4.0", default-features = false, features = [ + "wayland-data-control", +] } +base64 = "=0.22.1" +cbc = { version = "=0.1.2", features = ["alloc"] } +rand = "=0.8.5" +retry = "=2.0.0" +scopeguard = "=1.2.0" +sha2 = "=0.10.8" +thiserror = "=1.0.61" +typenum = "=1.17.0" + +[target.'cfg(windows)'.dependencies] +widestring = "=1.1.0" +windows = { version = "=0.57.0", features = [ + "Foundation", + "Security_Credentials_UI", + "Security_Cryptography", + "Storage_Streams", + "Win32_Foundation", + "Win32_Security_Credentials", + "Win32_System_WinRT", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_WindowsAndMessaging", +] } + +[target.'cfg(windows)'.dev-dependencies] +keytar = "=0.1.6" + +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "=0.9.4" +security-framework = "=2.11.0" +security-framework-sys = "=2.11.0" + +[target.'cfg(target_os = "linux")'.dependencies] +gio = "=0.19.5" +libsecret = "=0.5.0" diff --git a/apps/desktop/desktop_native/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs similarity index 94% rename from apps/desktop/desktop_native/src/biometric/macos.rs rename to apps/desktop/desktop_native/core/src/biometric/macos.rs index 9f0ecf83de7..858615d2e7e 100644 --- a/apps/desktop/desktop_native/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/macos.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Result}; -use crate::biometrics::{KeyMaterial, OsDerivedKey}; +use crate::biometric::{KeyMaterial, OsDerivedKey}; /// The MacOS implementation of the biometric trait. pub struct Biometric {} diff --git a/apps/desktop/desktop_native/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs similarity index 80% rename from apps/desktop/desktop_native/src/biometric/mod.rs rename to apps/desktop/desktop_native/core/src/biometric/mod.rs index 0280647e8ea..f61c4f04443 100644 --- a/apps/desktop/desktop_native/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -7,7 +7,15 @@ mod biometric; pub use biometric::Biometric; -use crate::biometrics::{KeyMaterial, OsDerivedKey}; +pub struct KeyMaterial { + pub os_key_part_b64: String, + pub client_key_part_b64: Option, +} + +pub struct OsDerivedKey { + pub key_b64: String, + pub iv_b64: String, +} pub trait BiometricTrait { fn prompt(hwnd: Vec, message: String) -> Result; diff --git a/apps/desktop/desktop_native/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs similarity index 94% rename from apps/desktop/desktop_native/src/biometric/unix.rs rename to apps/desktop/desktop_native/core/src/biometric/unix.rs index 8b9d1a84bbb..f9fe1ba57ca 100644 --- a/apps/desktop/desktop_native/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -1,6 +1,6 @@ use anyhow::{bail, Result}; -use crate::biometrics::{KeyMaterial, OsDerivedKey}; +use crate::biometric::{KeyMaterial, OsDerivedKey}; /// The Unix implementation of the biometric trait. pub struct Biometric {} diff --git a/apps/desktop/desktop_native/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs similarity index 99% rename from apps/desktop/desktop_native/src/biometric/windows.rs rename to apps/desktop/desktop_native/core/src/biometric/windows.rs index cb3b69273dd..1f5929a3ada 100644 --- a/apps/desktop/desktop_native/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -29,7 +29,7 @@ use windows::{ }; use crate::{ - biometrics::{KeyMaterial, OsDerivedKey}, + biometric::{KeyMaterial, OsDerivedKey}, crypto::{self, CipherString}, }; diff --git a/apps/desktop/desktop_native/src/clipboard.rs b/apps/desktop/desktop_native/core/src/clipboard.rs similarity index 100% rename from apps/desktop/desktop_native/src/clipboard.rs rename to apps/desktop/desktop_native/core/src/clipboard.rs diff --git a/apps/desktop/desktop_native/src/crypto/cipher_string.rs b/apps/desktop/desktop_native/core/src/crypto/cipher_string.rs similarity index 100% rename from apps/desktop/desktop_native/src/crypto/cipher_string.rs rename to apps/desktop/desktop_native/core/src/crypto/cipher_string.rs diff --git a/apps/desktop/desktop_native/src/crypto/crypto.rs b/apps/desktop/desktop_native/core/src/crypto/crypto.rs similarity index 100% rename from apps/desktop/desktop_native/src/crypto/crypto.rs rename to apps/desktop/desktop_native/core/src/crypto/crypto.rs diff --git a/apps/desktop/desktop_native/src/crypto/mod.rs b/apps/desktop/desktop_native/core/src/crypto/mod.rs similarity index 100% rename from apps/desktop/desktop_native/src/crypto/mod.rs rename to apps/desktop/desktop_native/core/src/crypto/mod.rs diff --git a/apps/desktop/desktop_native/src/error.rs b/apps/desktop/desktop_native/core/src/error.rs similarity index 100% rename from apps/desktop/desktop_native/src/error.rs rename to apps/desktop/desktop_native/core/src/error.rs diff --git a/apps/desktop/desktop_native/core/src/lib.rs b/apps/desktop/desktop_native/core/src/lib.rs new file mode 100644 index 00000000000..45933060270 --- /dev/null +++ b/apps/desktop/desktop_native/core/src/lib.rs @@ -0,0 +1,5 @@ +pub mod biometric; +pub mod clipboard; +pub mod crypto; +pub mod error; +pub mod password; diff --git a/apps/desktop/desktop_native/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs similarity index 100% rename from apps/desktop/desktop_native/src/password/macos.rs rename to apps/desktop/desktop_native/core/src/password/macos.rs diff --git a/apps/desktop/desktop_native/src/password/mod.rs b/apps/desktop/desktop_native/core/src/password/mod.rs similarity index 100% rename from apps/desktop/desktop_native/src/password/mod.rs rename to apps/desktop/desktop_native/core/src/password/mod.rs diff --git a/apps/desktop/desktop_native/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs similarity index 100% rename from apps/desktop/desktop_native/src/password/unix.rs rename to apps/desktop/desktop_native/core/src/password/unix.rs diff --git a/apps/desktop/desktop_native/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs similarity index 100% rename from apps/desktop/desktop_native/src/password/windows.rs rename to apps/desktop/desktop_native/core/src/password/windows.rs diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml new file mode 100644 index 00000000000..942ccdba212 --- /dev/null +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -0,0 +1,23 @@ +[package] +edition = "2021" +exclude = ["index.node"] +license = "GPL-3.0" +name = "desktop_napi" +version = "0.0.0" +publish = false + +[lib] +crate-type = ["cdylib"] + +[features] +default = [] +manual_test = [] + +[dependencies] +anyhow = "=1.0.86" +desktop_core = { path = "../core" } +napi = { version = "=2.16.6", features = ["async"] } +napi-derive = "=2.16.5" + +[build-dependencies] +napi-build = "=2.1.3" diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/napi/build.js similarity index 85% rename from apps/desktop/desktop_native/build.js rename to apps/desktop/desktop_native/napi/build.js index a6fc633afd4..6c92dbad1b6 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/napi/build.js @@ -14,6 +14,8 @@ switch (process.platform) { default: targets = ['x86_64-unknown-linux-musl']; + process.env["PKG_CONFIG_ALLOW_CROSS"] = "1"; + process.env["PKG_CONFIG_ALL_STATIC"] = "1"; break; } diff --git a/apps/desktop/desktop_native/build.rs b/apps/desktop/desktop_native/napi/build.rs similarity index 100% rename from apps/desktop/desktop_native/build.rs rename to apps/desktop/desktop_native/napi/build.rs diff --git a/apps/desktop/desktop_native/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts similarity index 100% rename from apps/desktop/desktop_native/index.d.ts rename to apps/desktop/desktop_native/napi/index.d.ts diff --git a/apps/desktop/desktop_native/index.js b/apps/desktop/desktop_native/napi/index.js similarity index 63% rename from apps/desktop/desktop_native/index.js rename to apps/desktop/desktop_native/napi/index.js index 1cf7ea9434e..75617ee2f1a 100644 --- a/apps/desktop/desktop_native/index.js +++ b/apps/desktop/desktop_native/napi/index.js @@ -25,24 +25,24 @@ switch (platform) { case 'android': switch (arch) { case 'arm64': - localFileExisted = existsSync(join(__dirname, 'desktop_native.android-arm64.node')) + localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm64.node')) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.android-arm64.node') + nativeBinding = require('./desktop_napi.android-arm64.node') } else { - nativeBinding = require('@bitwarden/desktop-native-android-arm64') + nativeBinding = require('@bitwarden/desktop-napi-android-arm64') } } catch (e) { loadError = e } break case 'arm': - localFileExisted = existsSync(join(__dirname, 'desktop_native.android-arm-eabi.node')) + localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm-eabi.node')) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.android-arm-eabi.node') + nativeBinding = require('./desktop_napi.android-arm-eabi.node') } else { - nativeBinding = require('@bitwarden/desktop-native-android-arm-eabi') + nativeBinding = require('@bitwarden/desktop-napi-android-arm-eabi') } } catch (e) { loadError = e @@ -56,13 +56,13 @@ switch (platform) { switch (arch) { case 'x64': localFileExisted = existsSync( - join(__dirname, 'desktop_native.win32-x64-msvc.node') + join(__dirname, 'desktop_napi.win32-x64-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.win32-x64-msvc.node') + nativeBinding = require('./desktop_napi.win32-x64-msvc.node') } else { - nativeBinding = require('@bitwarden/desktop-native-win32-x64-msvc') + nativeBinding = require('@bitwarden/desktop-napi-win32-x64-msvc') } } catch (e) { loadError = e @@ -70,13 +70,13 @@ switch (platform) { break case 'ia32': localFileExisted = existsSync( - join(__dirname, 'desktop_native.win32-ia32-msvc.node') + join(__dirname, 'desktop_napi.win32-ia32-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.win32-ia32-msvc.node') + nativeBinding = require('./desktop_napi.win32-ia32-msvc.node') } else { - nativeBinding = require('@bitwarden/desktop-native-win32-ia32-msvc') + nativeBinding = require('@bitwarden/desktop-napi-win32-ia32-msvc') } } catch (e) { loadError = e @@ -84,13 +84,13 @@ switch (platform) { break case 'arm64': localFileExisted = existsSync( - join(__dirname, 'desktop_native.win32-arm64-msvc.node') + join(__dirname, 'desktop_napi.win32-arm64-msvc.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.win32-arm64-msvc.node') + nativeBinding = require('./desktop_napi.win32-arm64-msvc.node') } else { - nativeBinding = require('@bitwarden/desktop-native-win32-arm64-msvc') + nativeBinding = require('@bitwarden/desktop-napi-win32-arm64-msvc') } } catch (e) { loadError = e @@ -103,12 +103,12 @@ switch (platform) { case 'darwin': switch (arch) { case 'x64': - localFileExisted = existsSync(join(__dirname, 'desktop_native.darwin-x64.node')) + localFileExisted = existsSync(join(__dirname, 'desktop_napi.darwin-x64.node')) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.darwin-x64.node') + nativeBinding = require('./desktop_napi.darwin-x64.node') } else { - nativeBinding = require('@bitwarden/desktop-native-darwin-x64') + nativeBinding = require('@bitwarden/desktop-napi-darwin-x64') } } catch (e) { loadError = e @@ -116,13 +116,13 @@ switch (platform) { break case 'arm64': localFileExisted = existsSync( - join(__dirname, 'desktop_native.darwin-arm64.node') + join(__dirname, 'desktop_napi.darwin-arm64.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.darwin-arm64.node') + nativeBinding = require('./desktop_napi.darwin-arm64.node') } else { - nativeBinding = require('@bitwarden/desktop-native-darwin-arm64') + nativeBinding = require('@bitwarden/desktop-napi-darwin-arm64') } } catch (e) { loadError = e @@ -136,12 +136,12 @@ switch (platform) { if (arch !== 'x64') { throw new Error(`Unsupported architecture on FreeBSD: ${arch}`) } - localFileExisted = existsSync(join(__dirname, 'desktop_native.freebsd-x64.node')) + localFileExisted = existsSync(join(__dirname, 'desktop_napi.freebsd-x64.node')) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.freebsd-x64.node') + nativeBinding = require('./desktop_napi.freebsd-x64.node') } else { - nativeBinding = require('@bitwarden/desktop-native-freebsd-x64') + nativeBinding = require('@bitwarden/desktop-napi-freebsd-x64') } } catch (e) { loadError = e @@ -151,13 +151,13 @@ switch (platform) { switch (arch) { case 'x64': localFileExisted = existsSync( - join(__dirname, 'desktop_native.linux-x64-musl.node') + join(__dirname, 'desktop_napi.linux-x64-musl.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.linux-x64-musl.node') + nativeBinding = require('./desktop_napi.linux-x64-musl.node') } else { - nativeBinding = require('@bitwarden/desktop-native-linux-x64-musl') + nativeBinding = require('@bitwarden/desktop-napi-linux-x64-musl') } } catch (e) { loadError = e @@ -165,13 +165,13 @@ switch (platform) { break case 'arm64': localFileExisted = existsSync( - join(__dirname, 'desktop_native.linux-arm64-musl.node') + join(__dirname, 'desktop_napi.linux-arm64-musl.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.linux-arm64-musl.node') + nativeBinding = require('./desktop_napi.linux-arm64-musl.node') } else { - nativeBinding = require('@bitwarden/desktop-native-linux-arm64-musl') + nativeBinding = require('@bitwarden/desktop-napi-linux-arm64-musl') } } catch (e) { loadError = e @@ -179,13 +179,13 @@ switch (platform) { break case 'arm': localFileExisted = existsSync( - join(__dirname, 'desktop_native.linux-arm-gnueabihf.node') + join(__dirname, 'desktop_napi.linux-arm-gnueabihf.node') ) try { if (localFileExisted) { - nativeBinding = require('./desktop_native.linux-arm-gnueabihf.node') + nativeBinding = require('./desktop_napi.linux-arm-gnueabihf.node') } else { - nativeBinding = require('@bitwarden/desktop-native-linux-arm-gnueabihf') + nativeBinding = require('@bitwarden/desktop-napi-linux-arm-gnueabihf') } } catch (e) { loadError = e diff --git a/apps/desktop/desktop_native/package.json b/apps/desktop/desktop_native/napi/package.json similarity index 90% rename from apps/desktop/desktop_native/package.json rename to apps/desktop/desktop_native/napi/package.json index 44958043707..70e472b3952 100644 --- a/apps/desktop/desktop_native/package.json +++ b/apps/desktop/desktop_native/napi/package.json @@ -1,5 +1,5 @@ { - "name": "@bitwarden/desktop-native", + "name": "@bitwarden/desktop-napi", "version": "0.1.0", "description": "", "scripts": { @@ -14,7 +14,7 @@ "@napi-rs/cli": "2.16.2" }, "napi": { - "name": "desktop_native", + "name": "desktop_napi", "triples": { "defaults": true, "additional": [ diff --git a/apps/desktop/desktop_native/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs similarity index 67% rename from apps/desktop/desktop_native/src/lib.rs rename to apps/desktop/desktop_native/napi/src/lib.rs index d00b95c47f2..e2e7eb7244f 100644 --- a/apps/desktop/desktop_native/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1,25 +1,19 @@ #[macro_use] extern crate napi_derive; -mod biometric; -mod clipboard; -mod crypto; -mod error; -mod password; - #[napi] pub mod passwords { /// Fetch the stored password from the keychain. #[napi] pub async fn get_password(service: String, account: String) -> napi::Result { - super::password::get_password(&service, &account) + desktop_core::password::get_password(&service, &account) .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Fetch the stored password from the keychain that was stored with Keytar. #[napi] pub async fn get_password_keytar(service: String, account: String) -> napi::Result { - super::password::get_password_keytar(&service, &account) + desktop_core::password::get_password_keytar(&service, &account) .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -30,21 +24,21 @@ pub mod passwords { account: String, password: String, ) -> napi::Result<()> { - super::password::set_password(&service, &account, &password) + desktop_core::password::set_password(&service, &account, &password) .map_err(|e| napi::Error::from_reason(e.to_string())) } /// Delete the stored password from the keychain. #[napi] pub async fn delete_password(service: String, account: String) -> napi::Result<()> { - super::password::delete_password(&service, &account) + desktop_core::password::delete_password(&service, &account) .map_err(|e| napi::Error::from_reason(e.to_string())) } } #[napi] pub mod biometrics { - use super::biometric::{Biometric, BiometricTrait}; + use desktop_core::biometric::{Biometric, BiometricTrait}; // Prompt for biometric confirmation #[napi] @@ -68,8 +62,14 @@ pub mod biometrics { key_material: Option, iv_b64: String, ) -> napi::Result { - Biometric::set_biometric_secret(&service, &account, &secret, key_material, &iv_b64) - .map_err(|e| napi::Error::from_reason(e.to_string())) + Biometric::set_biometric_secret( + &service, + &account, + &secret, + key_material.map(|m| m.into()), + &iv_b64, + ) + .map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] @@ -78,8 +78,9 @@ pub mod biometrics { account: String, key_material: Option, ) -> napi::Result { - let result = Biometric::get_biometric_secret(&service, &account, key_material) - .map_err(|e| napi::Error::from_reason(e.to_string())); + let result = + Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into())) + .map_err(|e| napi::Error::from_reason(e.to_string())); result } @@ -93,6 +94,7 @@ pub mod biometrics { #[napi] pub async fn derive_key_material(iv: Option) -> napi::Result { Biometric::derive_key_material(iv.as_deref()) + .map(|k| k.into()) .map_err(|e| napi::Error::from_reason(e.to_string())) } @@ -102,23 +104,41 @@ pub mod biometrics { pub client_key_part_b64: Option, } + impl From for desktop_core::biometric::KeyMaterial { + fn from(km: KeyMaterial) -> Self { + desktop_core::biometric::KeyMaterial { + os_key_part_b64: km.os_key_part_b64, + client_key_part_b64: km.client_key_part_b64, + } + } + } + #[napi(object)] pub struct OsDerivedKey { pub key_b64: String, pub iv_b64: String, } + + impl From for OsDerivedKey { + fn from(km: desktop_core::biometric::OsDerivedKey) -> Self { + OsDerivedKey { + key_b64: km.key_b64, + iv_b64: km.iv_b64, + } + } + } } #[napi] pub mod clipboards { #[napi] pub async fn read() -> napi::Result { - super::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) + desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string())) } #[napi] pub async fn write(text: String, password: bool) -> napi::Result<()> { - super::clipboard::write(&text, password) + desktop_core::clipboard::write(&text, password) .map_err(|e| napi::Error::from_reason(e.to_string())) } } diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 85fd88657ce..6b1d52919ae 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -15,16 +15,16 @@ "asarUnpack": ["**/*.node"], "files": [ "**/*", - "!**/node_modules/@bitwarden/desktop-native/**/*", - "**/node_modules/@bitwarden/desktop-native/index.js", - "**/node_modules/@bitwarden/desktop-native/desktop_native.${platform}-${arch}*.node", + "!**/node_modules/@bitwarden/desktop-napi/**/*", + "**/node_modules/@bitwarden/desktop-napi/index.js", + "**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node", "!**/node_modules/argon2/**/*", "**/node_modules/argon2/argon2.cjs", "**/node_modules/argon2/package.json", "**/node_modules/argon2/build/Release/argon2.node" ], - "electronVersion": "30.1.2", + "electronVersion": "31.2.0", "generateUpdatesFilesForAllChannels": true, "publish": { "provider": "generic", @@ -71,7 +71,7 @@ ], "CFBundleDevelopmentRegion": "en" }, - "singleArchFiles": "node_modules/@bitwarden/desktop-native/desktop_native.darwin-*.node", + "singleArchFiles": "node_modules/@bitwarden/desktop-napi/desktop_napi.darwin-*.node", "target": ["dmg", "zip"] }, "win": { diff --git a/apps/desktop/native-messaging-test-runner/src/variables.ts b/apps/desktop/native-messaging-test-runner/src/variables.ts index 6f0351ed6da..3f84a870852 100644 --- a/apps/desktop/native-messaging-test-runner/src/variables.ts +++ b/apps/desktop/native-messaging-test-runner/src/variables.ts @@ -1,5 +1,5 @@ export const applicationName = "Native Messaging Test Runner"; -export const encryptionAlgorithm = "sha1"; +export const hashAlgorithmForEncryption = "sha1"; export const testRsaPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl0Vawl/toXzkEvB82FEtqHP" + "4xlU2ab/v0crqIfXfIoWF/XXdHGIdrZeilnRXPPJT1B9dTsasttEZNnua/0Rek/cjNDHtzT52irfoZYS7X6HNIfOi54Q+egP" + diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 697ab88ae98..f1639dc51a1 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.6.6", + "version": "2024.7.1", "keywords": [ "bitwarden", "password", @@ -18,7 +18,7 @@ "scripts": { "postinstall": "electron-rebuild", "start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build", - "build-native": "cd desktop_native && npm run build", + "build-native": "cd desktop_native/napi && npm run build", "build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"", "build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"", "build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js", diff --git a/apps/desktop/src/app/accounts/vault-timeout-input.component.ts b/apps/desktop/src/app/accounts/vault-timeout-input.component.ts index 75c3c9e9e0e..c56e6578a0b 100644 --- a/apps/desktop/src/app/accounts/vault-timeout-input.component.ts +++ b/apps/desktop/src/app/accounts/vault-timeout-input.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { NG_VALIDATORS, NG_VALUE_ACCESSOR } from "@angular/forms"; -import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/angular/components/settings/vault-timeout-input.component"; +import { VaultTimeoutInputComponent as VaultTimeoutInputComponentBase } from "@bitwarden/auth/angular"; @Component({ selector: "app-vault-timeout-input", diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index c7a66f510ca..0e3f10345a1 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -19,6 +19,7 @@ import { } from "@bitwarden/auth/angular"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard"; import { HintComponent } from "../auth/hint.component"; @@ -30,6 +31,7 @@ import { RegisterComponent } from "../auth/register.component"; import { RemovePasswordComponent } from "../auth/remove-password.component"; import { SetPasswordComponent } from "../auth/set-password.component"; import { SsoComponent } from "../auth/sso.component"; +import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component"; import { TwoFactorComponent } from "../auth/two-factor.component"; import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component"; import { VaultComponent } from "../vault/app/vault/vault.component"; @@ -61,7 +63,24 @@ const routes: Routes = [ path: "admin-approval-requested", component: LoginViaAuthRequestComponent, }, - { path: "2fa", component: TwoFactorComponent }, + ...twofactorRefactorSwap( + TwoFactorComponent, + AnonLayoutWrapperComponent, + { + path: "2fa", + }, + { + path: "2fa", + component: AnonLayoutWrapperComponent, + children: [ + { + path: "", + component: TwoFactorAuthComponent, + canActivate: [unauthGuardFn()], + }, + ], + }, + ), { path: "login-initiated", component: LoginDecryptionOptionsComponent, diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index e4fdd17dc15..a6499f6f82b 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -43,7 +43,6 @@ import { BiometricStateService } from "@bitwarden/common/platform/biometrics/bio import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UserId } from "@bitwarden/common/types/guid"; import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -51,6 +50,7 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; import { DialogService, ToastOptions, ToastService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DeleteAccountComponent } from "../auth/delete-account.component"; import { LoginApprovalComponent } from "../auth/login/login-approval.component"; @@ -403,7 +403,6 @@ export class AppComponent implements OnInit, OnDestroy { // Clear sequentialized caches clearCaches(); if (message.userId != null) { - await this.stateService.clearDecryptedData(message.userId); await this.accountService.switchAccount(message.userId); } const locked = @@ -423,12 +422,13 @@ export class AppComponent implements OnInit, OnDestroy { } else { this.messagingService.send("unlocked"); this.loading = true; - await this.syncService.fullSync(true); + await this.syncService.fullSync(false); this.loading = false; // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["vault"]); } + this.messagingService.send("finishSwitchAccount"); break; } case "systemSuspended": diff --git a/apps/desktop/src/app/layout/account-switcher.component.html b/apps/desktop/src/app/layout/account-switcher.component.html index b5741a1a1b7..db40da41cba 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.html +++ b/apps/desktop/src/app/layout/account-switcher.component.html @@ -7,6 +7,7 @@ cdkOverlayOrigin #trigger="cdkOverlayOrigin" [hidden]="!view.showSwitcher" + [disabled]="disabled" aria-haspopup="dialog" > diff --git a/apps/desktop/src/app/layout/account-switcher.component.ts b/apps/desktop/src/app/layout/account-switcher.component.ts index 92cfebfd605..f641d801b8d 100644 --- a/apps/desktop/src/app/layout/account-switcher.component.ts +++ b/apps/desktop/src/app/layout/account-switcher.component.ts @@ -12,6 +12,7 @@ import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authenticatio 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 { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; import { UserId } from "@bitwarden/common/types/guid"; type ActiveAccount = { @@ -75,12 +76,14 @@ export class AccountSwitcherComponent { showSwitcher$: Observable; numberOfAccounts$: Observable; + disabled = false; constructor( private stateService: StateService, private authService: AuthService, private avatarService: AvatarService, private messagingService: MessagingService, + private messageListener: MessageListener, private router: Router, private environmentService: EnvironmentService, private loginEmailService: LoginEmailServiceAbstraction, @@ -159,19 +162,20 @@ export class AccountSwitcherComponent { async switch(userId: string) { this.close(); - this.messagingService.send("switchAccount", { userId: userId }); + this.disabled = true; + const accountSwitchFinishedPromise = firstValueFrom( + this.messageListener.messages$(new CommandDefinition("finishSwitchAccount")), + ); + this.messagingService.send("switchAccount", { userId }); + await accountSwitchFinishedPromise; + this.disabled = false; } async addAccount() { this.close(); - this.loginEmailService.setRememberEmail(false); - await this.loginEmailService.saveEmailSettings(); - - await this.router.navigate(["/login"]); - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); - await this.stateService.clearDecryptedData(activeAccount?.id as UserId); await this.accountService.switchAccount(null); + await this.router.navigate(["/login"]); } private async createInactiveAccounts(baseAccounts: { diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 25d4df5f935..dfea2e6f274 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -13,7 +13,6 @@ import { SUPPORTS_SECURE_STORAGE, SYSTEM_THEME_OBSERVABLE, SafeInjectionToken, - STATE_FACTORY, DEFAULT_VAULT_TIMEOUT, INTRAPROCESS_MESSAGING_SUBJECT, CLIENT_TYPE, @@ -25,13 +24,11 @@ import { AccountService as AccountServiceAbstraction } from "@bitwarden/common/a 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 { 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"; -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 as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -45,25 +42,21 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/platf import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service"; 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"; import { SystemService } from "@bitwarden/common/platform/services/system.service"; import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state"; // eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; -import { Account } from "../../models/account"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { ElectronCryptoService } from "../../platform/services/electron-crypto.service"; import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service"; @@ -74,7 +67,6 @@ import { 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"; @@ -90,11 +82,6 @@ import { RendererCryptoFunctionService } from "./renderer-crypto-function.servic const RELOAD_CALLBACK = new SafeInjectionToken<() => any>("RELOAD_CALLBACK"); -// Desktop has its own Account definition which must be used in its StateService -const DESKTOP_STATE_FACTORY = new SafeInjectionToken>( - "DESKTOP_STATE_FACTORY", -); - /** * Provider definitions used in the ngModule. * Add your provider definition here using the safeProvider function as a wrapper. This will give you type safety. @@ -111,14 +98,6 @@ const safeProviders: SafeProvider[] = [ deps: [InitService], multi: true, }), - safeProvider({ - provide: DESKTOP_STATE_FACTORY, - useValue: new StateFactory(GlobalState, Account), - }), - safeProvider({ - provide: STATE_FACTORY, - useValue: null, - }), safeProvider({ provide: RELOAD_CALLBACK, useValue: null, @@ -194,28 +173,12 @@ const safeProviders: SafeProvider[] = [ MessagingServiceAbstraction, PlatformUtilsServiceAbstraction, RELOAD_CALLBACK, - StateServiceAbstraction, AutofillSettingsServiceAbstraction, VaultTimeoutSettingsService, BiometricStateService, AccountServiceAbstraction, ], }), - safeProvider({ - provide: StateServiceAbstraction, - useClass: ElectronStateService, - deps: [ - AbstractStorageService, - SECURE_STORAGE, - MEMORY_STORAGE, - LogService, - DESKTOP_STATE_FACTORY, - AccountServiceAbstraction, - EnvironmentService, - TokenService, - MigrationRunner, - ], - }), safeProvider({ provide: FileDownloadService, useClass: DesktopFileDownloadService, diff --git a/apps/desktop/src/app/tools/generator.component.spec.ts b/apps/desktop/src/app/tools/generator.component.spec.ts index db8160c2025..c259b4e52ce 100644 --- a/apps/desktop/src/app/tools/generator.component.spec.ts +++ b/apps/desktop/src/app/tools/generator.component.spec.ts @@ -8,10 +8,12 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { ToastService } from "@bitwarden/components"; +import { + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; import { GeneratorComponent } from "./generator.component"; diff --git a/apps/desktop/src/app/tools/generator.component.ts b/apps/desktop/src/app/tools/generator.component.ts index 0f1443314dc..fc9ff489a1a 100644 --- a/apps/desktop/src/app/tools/generator.component.ts +++ b/apps/desktop/src/app/tools/generator.component.ts @@ -6,9 +6,11 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; -import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; import { ToastService } from "@bitwarden/components"; +import { + PasswordGenerationServiceAbstraction, + UsernameGenerationServiceAbstraction, +} from "@bitwarden/generator-legacy"; @Component({ selector: "app-generator", diff --git a/apps/desktop/src/app/tools/password-generator-history.component.ts b/apps/desktop/src/app/tools/password-generator-history.component.ts index 07ebe5fb4bd..0c7c9c4e221 100644 --- a/apps/desktop/src/app/tools/password-generator-history.component.ts +++ b/apps/desktop/src/app/tools/password-generator-history.component.ts @@ -3,8 +3,8 @@ import { Component } from "@angular/core"; import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { ToastService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ selector: "app-password-generator-history", 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 40d41f22361..7a8dfcbcda7 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 @@ -22,8 +22,8 @@ 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 { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; diff --git a/apps/desktop/src/auth/login/login.component.html b/apps/desktop/src/auth/login/login.component.html index d9983220745..8a8611ad03d 100644 --- a/apps/desktop/src/auth/login/login.component.html +++ b/apps/desktop/src/auth/login/login.component.html @@ -48,7 +48,7 @@

{{ "newAroundHere" | i18n }}

-
@@ -99,7 +99,7 @@ class="btn block" type="button" routerLink="/accessibility-cookie" - (click)="setLoginEmailValues()" + (click)="saveEmailSettings()" > {{ "loadAccessibilityCookie" | i18n }} @@ -139,7 +139,7 @@ type="button" class="text text-primary password-hint-btn" routerLink="/hint" - (click)="setLoginEmailValues()" + (click)="saveEmailSettings()" > {{ "getMasterPasswordHint" | i18n }} diff --git a/apps/desktop/src/auth/login/login.component.ts b/apps/desktop/src/auth/login/login.component.ts index fd57e9015b1..827f7f5287e 100644 --- a/apps/desktop/src/auth/login/login.component.ts +++ b/apps/desktop/src/auth/login/login.component.ts @@ -9,13 +9,13 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,8 +23,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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { EnvironmentComponent } from "../environment.component"; @@ -72,7 +72,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, ) { super( devicesApiService, @@ -93,7 +93,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, ); super.onSuccessfulLogin = () => { return syncService.fullSync(true); diff --git a/apps/desktop/src/auth/register.component.ts b/apps/desktop/src/auth/register.component.ts index 82b815aa186..be44c276485 100644 --- a/apps/desktop/src/auth/register.component.ts +++ b/apps/desktop/src/auth/register.component.ts @@ -14,8 +14,8 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "RegisterComponent"; diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index feea5edd867..3012f646036 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -19,10 +19,10 @@ 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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; const BroadcasterSubscriptionId = "SetPasswordComponent"; diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index cc261f1235c..234ebc85cee 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -17,8 +17,8 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; @Component({ selector: "app-sso", diff --git a/apps/desktop/src/auth/two-factor-auth.component.ts b/apps/desktop/src/auth/two-factor-auth.component.ts new file mode 100644 index 00000000000..191a88e621a --- /dev/null +++ b/apps/desktop/src/auth/two-factor-auth.component.ts @@ -0,0 +1,43 @@ +import { DialogModule } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { ReactiveFormsModule } from "@angular/forms"; +import { RouterLink } from "@angular/router"; + +import { TwoFactorAuthAuthenticatorComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-authenticator.component"; +import { TwoFactorAuthYubikeyComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth-yubikey.component"; +import { TwoFactorAuthComponent as BaseTwoFactorAuthComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component"; +import { TwoFactorOptionsComponent } from "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-options.component"; +import { JslibModule } from "../../../../libs/angular/src/jslib.module"; +import { AsyncActionsModule } from "../../../../libs/components/src/async-actions"; +import { ButtonModule } from "../../../../libs/components/src/button"; +import { CheckboxModule } from "../../../../libs/components/src/checkbox"; +import { FormFieldModule } from "../../../../libs/components/src/form-field"; +import { LinkModule } from "../../../../libs/components/src/link"; +import { I18nPipe } from "../../../../libs/components/src/shared/i18n.pipe"; +import { TypographyModule } from "../../../../libs/components/src/typography"; + +@Component({ + standalone: true, + templateUrl: + "../../../../libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.html", + selector: "app-two-factor-auth", + imports: [ + CommonModule, + JslibModule, + DialogModule, + ButtonModule, + LinkModule, + TypographyModule, + ReactiveFormsModule, + FormFieldModule, + AsyncActionsModule, + RouterLink, + CheckboxModule, + TwoFactorOptionsComponent, + TwoFactorAuthAuthenticatorComponent, + TwoFactorAuthYubikeyComponent, + ], + providers: [I18nPipe], +}) +export class TwoFactorAuthComponent extends BaseTwoFactorAuthComponent {} diff --git a/apps/desktop/src/images/two-factor/0.png b/apps/desktop/src/images/two-factor/0.png index f37e3f17b4d..307ff4fd60f 100644 Binary files a/apps/desktop/src/images/two-factor/0.png and b/apps/desktop/src/images/two-factor/0.png differ diff --git a/apps/desktop/src/images/two-factor/1-w.png b/apps/desktop/src/images/two-factor/1-w.png new file mode 100644 index 00000000000..a4e39b3f466 Binary files /dev/null and b/apps/desktop/src/images/two-factor/1-w.png differ diff --git a/apps/desktop/src/images/two-factor/1.png b/apps/desktop/src/images/two-factor/1.png index b47a12b1db8..37fb7bc4327 100644 Binary files a/apps/desktop/src/images/two-factor/1.png and b/apps/desktop/src/images/two-factor/1.png differ diff --git a/apps/desktop/src/images/two-factor/2.png b/apps/desktop/src/images/two-factor/2.png index ab2e4340361..d069bdab992 100644 Binary files a/apps/desktop/src/images/two-factor/2.png and b/apps/desktop/src/images/two-factor/2.png differ diff --git a/apps/desktop/src/images/two-factor/3.png b/apps/desktop/src/images/two-factor/3.png index 21aac2da678..c543343f53b 100644 Binary files a/apps/desktop/src/images/two-factor/3.png and b/apps/desktop/src/images/two-factor/3.png differ diff --git a/apps/desktop/src/images/two-factor/4.png b/apps/desktop/src/images/two-factor/4.png index ae7d7b55e49..058671ea37e 100644 Binary files a/apps/desktop/src/images/two-factor/4.png and b/apps/desktop/src/images/two-factor/4.png differ diff --git a/apps/desktop/src/images/two-factor/6.png b/apps/desktop/src/images/two-factor/6.png index ab2e4340361..d069bdab992 100644 Binary files a/apps/desktop/src/images/two-factor/6.png and b/apps/desktop/src/images/two-factor/6.png differ diff --git a/apps/desktop/src/images/two-factor/7-w.png b/apps/desktop/src/images/two-factor/7-w.png new file mode 100644 index 00000000000..89fdd8a2a08 Binary files /dev/null and b/apps/desktop/src/images/two-factor/7-w.png differ diff --git a/apps/desktop/src/images/two-factor/7.png b/apps/desktop/src/images/two-factor/7.png new file mode 100644 index 00000000000..2a38bdcd3e5 Binary files /dev/null and b/apps/desktop/src/images/two-factor/7.png differ diff --git a/apps/desktop/src/images/two-factor/rc-w.png b/apps/desktop/src/images/two-factor/rc-w.png new file mode 100644 index 00000000000..e83b8db1324 Binary files /dev/null and b/apps/desktop/src/images/two-factor/rc-w.png differ diff --git a/apps/desktop/src/images/two-factor/rc.png b/apps/desktop/src/images/two-factor/rc.png new file mode 100644 index 00000000000..4bebdf936cc Binary files /dev/null and b/apps/desktop/src/images/two-factor/rc.png differ diff --git a/apps/desktop/src/locales/af/messages.json b/apps/desktop/src/locales/af/messages.json index fdb978c8438..3e643925e7a 100644 --- a/apps/desktop/src/locales/af/messages.json +++ b/apps/desktop/src/locales/af/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Hoofwagwoordbevestiging stem nie ooreen nie." - }, - "newAccountCreated": { - "message": "U nuwe rekening is geskep! U kan nou aanteken." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Hoofwagwoordbevestiging stem nie ooreen nie." + }, + "newAccountCreated": { + "message": "U nuwe rekening is geskep! U kan nou aanteken." + }, "masterPassSent": { "message": "Ons het ’n e-pos gestuur met u hoofwagwoordwenk." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Waarmerktoep" }, - "authenticatorAppDesc": { - "message": "Gebruik ’n waarmerktoep (soos Authy of Google Authenticator) om tydgebaseerde bevestigingskodes optewek.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey-OTP-beveiligingsleutel" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Gebruik ’n YubiKey vir toegang tot u rekening. Werk met YubiKey 4, 4 Nano, 4C en NEO-toestelle." }, - "duoDesc": { - "message": "Bevestig met Duo Security d.m.v. die Duo Mobile-toep, SMS, spraakoproep of ’n U2F-beveiligingsleutel.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-pos" }, - "emailDesc": { - "message": "U sal bevestigingskodes per e-pos ontvang." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Aantekening onbeskikbaar" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/ar/messages.json b/apps/desktop/src/locales/ar/messages.json index 22e1f984fd5..b80ec1b4dc9 100644 --- a/apps/desktop/src/locales/ar/messages.json +++ b/apps/desktop/src/locales/ar/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "لا يتطابق تأكيد كلمة المرور مع كلمة المرور." - }, - "newAccountCreated": { - "message": "تم إنشاء حسابك الجديد! يمكنك الآن تسجيل الدخول." - }, "youSuccessfullyLoggedIn": { "message": "سجلتَ الدخول بنجاح" }, "youMayCloseThisWindow": { "message": "يمكنك إغلاق هذه النافذة" }, + "masterPassDoesntMatch": { + "message": "لا يتطابق تأكيد كلمة المرور مع كلمة المرور." + }, + "newAccountCreated": { + "message": "تم إنشاء حسابك الجديد! يمكنك الآن تسجيل الدخول." + }, "masterPassSent": { "message": "لقد أرسلنا لك رسالة بريد إلكتروني تحتوي على تلميحات كلمة المرور الرئيسية." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "تطبيق المصادقة" }, - "authenticatorAppDesc": { - "message": "استخدام تطبيق مصادقة (مثل Authy أو Google Authenticator) لإنشاء رموز تحقق مستندة إلى الوقت.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "مفتاح أمان YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "استخدم YubiKey للوصول إلى حسابك. يعمل مع YubiKey 4 ،4 Nano ،4C، وأجهزة NEO." }, - "duoDesc": { - "message": "التحقق باستخدام نظام الحماية الثنائي باستخدام تطبيق Duo Mobile أو الرسائل القصيرة أو المكالمة الهاتفية أو مفتاح الأمان U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "البريد الإلكتروني" }, - "emailDesc": { - "message": "سيتم إرسال رمز التحقق إليك بالبريد الإلكتروني." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "تسجيل الدخول غير متاح" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "تأكيد كلمة مرور الملف" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "تم إلغاء المصادقة المتعددة" }, diff --git a/apps/desktop/src/locales/az/messages.json b/apps/desktop/src/locales/az/messages.json index acc8df982f3..3481c03dfaa 100644 --- a/apps/desktop/src/locales/az/messages.json +++ b/apps/desktop/src/locales/az/messages.json @@ -113,7 +113,7 @@ "message": "Notlar" }, "customFields": { - "message": "Özəl sahələr" + "message": "Özəl xanalar" }, "launch": { "message": "Başlat" @@ -317,7 +317,7 @@ "message": "Qovluq" }, "newCustomField": { - "message": "Yeni özəl sahə" + "message": "Yeni özəl xana" }, "value": { "message": "Dəyər" @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Ana parolun təsdiqi uyuşmur." - }, - "newAccountCreated": { - "message": "Yeni hesabınız yaradıldı! İndi giriş edə bilərsiniz." - }, "youSuccessfullyLoggedIn": { "message": "Uğurla giriş etdiniz" }, "youMayCloseThisWindow": { "message": "Bu pəncərəni bağlaya bilərsiniz" }, + "masterPassDoesntMatch": { + "message": "Ana parolun təsdiqi uyuşmur." + }, + "newAccountCreated": { + "message": "Yeni hesabınız yaradıldı! İndi giriş edə bilərsiniz." + }, "masterPassSent": { "message": "Ana parol məsləhətini ehtiva edən bir e-poçt göndərdik." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Kimlik doğrulayıcı tətbiqi" }, - "authenticatorAppDesc": { - "message": "Vaxt əsaslı doğrulama kodları yaratmaq üçün (Authy və ya Google Authenticator kimi) kimlik doğrulayıcı tətbiq istifadə edin.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Bitwarden Authenticator kimi bir kimlik doğrulama tətbiqi tərəfindən yaradılan kodu daxil edin.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP güvənlik açarı" + "yubiKeyTitleV2": { + "message": "Yubico OTP güvənlik açarı" }, "yubiKeyDesc": { "message": "Hesabınıza müraciət etmək üçün bir YubiKey istifadə edin. YubiKey 4, 4 Nano, 4C və NEO cihazları ilə işləyir." }, - "duoDesc": { - "message": "Duo Security ilə doğrulamaq üçün Duo Mobile tətbiqi, SMS, telefon zəngi və ya U2F güvənlik açarını istifadə edin.", + "duoDescV2": { + "message": "Duo Security tərəfindən yaradılan kodu daxil edin.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-poçt" }, - "emailDesc": { - "message": "Doğrulama kodları e-poçt ünvanınıza göndəriləcək." + "emailDescV2": { + "message": "E-poçtunuza göndərilən kodu daxil edin." }, "loginUnavailable": { "message": "Giriş edilə bilmir" @@ -709,13 +709,13 @@ "message": "İki mərhələli giriş seçimləri" }, "selfHostedEnvironment": { - "message": "Öz-özünə sahiblik edən mühit" + "message": "Self-hosted mühit" }, "selfHostedEnvironmentFooter": { - "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının baza URL-sini müəyyənləşdirin." + "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının təməl URL-sini müəyyənləşdirin." }, "selfHostedBaseUrlHint": { - "message": "Şirkət daxili sahiblik edən Bitwarden quraşdırmasının təməl URL-sini qeyd edin. Nümunə: https://bitwarden.company.com" + "message": "Öz-özünə sahiblik edən Bitwarden quraşdırmasının təməl URL-sini müəyyənləşdirin. Nümunə: https://bitwarden.company.com" }, "selfHostedCustomEnvHeader": { "message": "Qabaqcıl konfiqurasiya üçün hər xidmətin təməl URL-sini müstəqil olaraq qeyd edə bilərsiniz." @@ -2085,7 +2085,7 @@ "message": "Ana parol silindi." }, "convertOrganizationEncryptionDesc": { - "message": "$ORGANIZATION$, öz-özünə sahiblik edən açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", + "message": "$ORGANIZATION$, self-hosted açar serveri ilə SSO istifadə edir. Bu təşkilatın üzvlərinin giriş etməsi üçün artıq ana parol tələb edilməyəcək.", "placeholders": { "organization": { "content": "$1", @@ -2576,7 +2576,7 @@ "message": "Giriş edilir" }, "selfHostedServer": { - "message": "öz-özünə sahiblik edən" + "message": "self-hosted" }, "accessDenied": { "message": "Müraciət rədd edildi. Bu səhifəyə baxmaq üçün icazəniz yoxdur." @@ -2670,7 +2670,7 @@ "message": "Giriş, bir e-poçt ünvanı deyil." }, "fieldsNeedAttention": { - "message": "Yuxarıdakı $COUNT$ sahənin diqqətinizə ehtiyacı var.", + "message": "Yuxarıdakı $COUNT$ xananın diqqətinizə ehtiyacı var.", "placeholders": { "count": { "content": "$1", @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Fayl parolunu təsdiqlə" }, + "exportSuccess": { + "message": "Anbar datası xaricə köçürüldü" + }, "multifactorAuthenticationCancelled": { "message": "Çox faktorlu kimlik doğrulama ləğv edildi" }, diff --git a/apps/desktop/src/locales/be/messages.json b/apps/desktop/src/locales/be/messages.json index c1095f8ee01..c1fa1119706 100644 --- a/apps/desktop/src/locales/be/messages.json +++ b/apps/desktop/src/locales/be/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Пацвярджэнне асноўнага пароля не супадае." - }, - "newAccountCreated": { - "message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Пацвярджэнне асноўнага пароля не супадае." + }, + "newAccountCreated": { + "message": "Ваш уліковы запіс створаны! Цяпер вы можаце ўвайсці ў яго." + }, "masterPassSent": { "message": "Мы адправілі вам на электронную пошту падказку да асноўнага пароля." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Праграма аўтэнтыфікацыі" }, - "authenticatorAppDesc": { - "message": "Выкарыстоўвайце праграму праграму аўтэнтыфікацыі (напрыклад, Authy або Google Authenticator) для генерацыі праверачных кодаў на падставе часу.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ бяспекі YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Выкарыстоўвайце YubiKey для доступу да вашага ўліковага запісу. Працуе з ключамі бяспекі YubiKey 4, 4 Nano, 4C і NEO." }, - "duoDesc": { - "message": "Праверка з дапамогай Duo Security, выкарыстоўваючы праграму Duo Mobile, SMS, тэлефонны выклік або ключ бяспекі U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Электронная пошта" }, - "emailDesc": { - "message": "Праверачныя коды будуць адпраўляцца вам па электронную пошту." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Уваход недаступны" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/bg/messages.json b/apps/desktop/src/locales/bg/messages.json index 0832eee1f96..23657992a5e 100644 --- a/apps/desktop/src/locales/bg/messages.json +++ b/apps/desktop/src/locales/bg/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Главната парола и потвърждението ѝ не съвпадат." - }, - "newAccountCreated": { - "message": "Абонаментът ви бе създаден. Вече можете да се впишете." - }, "youSuccessfullyLoggedIn": { "message": "Вписахте се успешно" }, "youMayCloseThisWindow": { "message": "Може да затворите този прозорец" }, + "masterPassDoesntMatch": { + "message": "Главната парола и потвърждението ѝ не съвпадат." + }, + "newAccountCreated": { + "message": "Абонаментът ви бе създаден. Вече можете да се впишете." + }, "masterPassSent": { "message": "Изпратили сме ви е-писмо с подсказка за главната ви парола." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Приложение за удостоверяване" }, - "authenticatorAppDesc": { - "message": "Използвайте приложение за удостоверяване (като Authy или Google Authenticator) за генерирането на временни кодове за потвърждение.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Въведете код създаден чрез приложение за удостоверяване, като например това на Битуорден.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Устройство YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Ключ за сигурност YubiKey OTP" }, "yubiKeyDesc": { "message": "Използвайте устройство на YubiKey, за да влезете в абонамента си. Поддържат се моделите YubiKey 4, 4 Nano, 4C и NEO." }, - "duoDesc": { - "message": "Удостоверяване чрез Duo Security, с ползване на приложението Duo Mobile, SMS, телефонен разговор или устройство U2F.", + "duoDescV2": { + "message": "Въведете код създаден чрез Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Електронна поща" }, - "emailDesc": { - "message": "Кодовете за потвърждение ще ви бъдат пратени по е-поща." + "emailDescV2": { + "message": "Въведете кода изпратен на е-пощата Ви." }, "loginUnavailable": { "message": "Записът липсва" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Потвърждаване на паролата на файла" }, + "exportSuccess": { + "message": "Данните от трезора са изнесени" + }, "multifactorAuthenticationCancelled": { "message": "Многостъпковото удостоверяване е отменено" }, diff --git a/apps/desktop/src/locales/bn/messages.json b/apps/desktop/src/locales/bn/messages.json index 4957136e081..a383dd0a328 100644 --- a/apps/desktop/src/locales/bn/messages.json +++ b/apps/desktop/src/locales/bn/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "মূল পাসওয়ার্ড নিশ্চিতকরণ মেলেনি।" - }, - "newAccountCreated": { - "message": "আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন প্রবেশ করতে পারেন।" - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "মূল পাসওয়ার্ড নিশ্চিতকরণ মেলেনি।" + }, + "newAccountCreated": { + "message": "আপনার নতুন অ্যাকাউন্ট তৈরি করা হয়েছে! আপনি এখন প্রবেশ করতে পারেন।" + }, "masterPassSent": { "message": "আমরা আপনাকে আপনার মূল পাসওয়ার্ডের ইঙ্গিতসহ একটি ইমেল প্রেরণ করেছি।" }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "প্রমাণীকরণকারী অ্যাপ" }, - "authenticatorAppDesc": { - "message": "সময় ভিত্তিক যাচাইকরণ কোড উৎপন্ন করতে একটি প্রমাণীকরণকারী অ্যাপ্লিকেশন (যেমন Authy বা Google Authenticator) ব্যবহার করুন।", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP সুরক্ষা কী" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "আপনার অ্যাকাউন্ট ব্যাবহার করতে একটি YubiKey ব্যবহার করুন। YubiKey 4, 4 Nano, 4C, এবং NEO ডিভাইসগুলির সাথে কাজ করে।" }, - "duoDesc": { - "message": "Duo Mobile app, এসএমএস, ফোন কল, বা U2F সুরক্ষা কী ব্যবহার করে Duo Security এর মাধ্যমে যাচাই করুন।", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "ইমেইল" }, - "emailDesc": { - "message": "যাচাই কোডগুলি আপনাকে ই-মেইল করা হবে।" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "লগইন অনুপলব্ধ" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/bs/messages.json b/apps/desktop/src/locales/bs/messages.json index d0d116499cf..a3e5c4dc2c4 100644 --- a/apps/desktop/src/locales/bs/messages.json +++ b/apps/desktop/src/locales/bs/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Potvrda glavne lozinke se ne podudara." - }, - "newAccountCreated": { - "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Potvrda glavne lozinke se ne podudara." + }, + "newAccountCreated": { + "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." + }, "masterPassSent": { "message": "Poslali smo vam e-mail sa podsjetnikom za glavnu lozinku." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplikacija za autentifikaciju" }, - "authenticatorAppDesc": { - "message": "Koristi aplikaciju za autentifikaciju (npr. Authy ili Google Authentifikator) za generiranje kontrolnih kodova.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP sigurnosni ključ" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Koristi YubiKey za pristup svom računu. Radi s YubiKey 4, 4 Nano, 4C i NEO uređajima." }, - "duoDesc": { - "message": "Potvrdi sa Duo Security pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-Mail " }, - "emailDesc": { - "message": "Verifikacijski kodovi će biti poslani E-Mailom." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Prijava nije moguća" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/ca/messages.json b/apps/desktop/src/locales/ca/messages.json index 661ffa91801..41ab3bba0a4 100644 --- a/apps/desktop/src/locales/ca/messages.json +++ b/apps/desktop/src/locales/ca/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "La confirmació de la contrasenya mestra no coincideix." - }, - "newAccountCreated": { - "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." - }, "youSuccessfullyLoggedIn": { "message": "Heu iniciat sessió correctament" }, "youMayCloseThisWindow": { "message": "Podeu tancar aquesta finestra" }, + "masterPassDoesntMatch": { + "message": "La confirmació de la contrasenya mestra no coincideix." + }, + "newAccountCreated": { + "message": "El vostre compte s'ha creat correctament. Ara ja podeu iniciar sessió." + }, "masterPassSent": { "message": "Hem enviat un correu electrònic amb la vostra contrasenya mestra." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplicació autenticadora" }, - "authenticatorAppDesc": { - "message": "Utilitzeu una aplicació autenticadora (com Authy o Google Authenticator) per generar codis de verificació basats en el temps.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Clau de seguretat OTP de YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Utilitzeu una YubiKey per accedir al vostre compte. Funciona amb els dispositius YubiKey 4, 4 Nano, 4C i NEO." }, - "duoDesc": { - "message": "Verifiqueu amb Duo Security mitjançant l'aplicació Duo Mobile, SMS, trucada telefònica o clau de seguretat U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Correu electrònic" }, - "emailDesc": { - "message": "Els codis de verificació els rebreu per correu electrònic." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Inici de sessió no disponible" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirma la contrasenya del fitxer" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "S'ha cancel·lat l'autenticació multifactor" }, diff --git a/apps/desktop/src/locales/cs/messages.json b/apps/desktop/src/locales/cs/messages.json index 285dd844c8d..dee3d75f694 100644 --- a/apps/desktop/src/locales/cs/messages.json +++ b/apps/desktop/src/locales/cs/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Potvrzení hlavního hesla se neshoduje." - }, - "newAccountCreated": { - "message": "Váš účet byl vytvořen! Můžete se přihlásit." - }, "youSuccessfullyLoggedIn": { "message": "Byli jste úspěšně přihlášeni" }, "youMayCloseThisWindow": { "message": "Toto okno můžete zavřít" }, + "masterPassDoesntMatch": { + "message": "Potvrzení hlavního hesla se neshoduje." + }, + "newAccountCreated": { + "message": "Váš účet byl vytvořen! Můžete se přihlásit." + }, "masterPassSent": { "message": "Poslali jsme vám e-mail s nápovědou k hlavnímu heslu." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Ověřovací aplikace" }, - "authenticatorAppDesc": { - "message": "Použijte ověřovací aplikaci (jako je Authy nebo Google Authenticator) pro generování časově omezených kódů.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Zadejte kód vygenerovaný ověřovací aplikací, jako je Autentikátor Bitwarden.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "Bezpečnostní klíč YubiKey OTP" }, "yubiKeyDesc": { "message": "Použije YubiKey pro přístup k Vašemu trezoru. Podporuje YubiKey 4, 4 Nano, 4C a NEO." }, - "duoDesc": { - "message": "Ověření pomocí Duo Security prostřednictvím aplikace Duo Mobile, SMS, telefonního hovoru nebo bezpečnostního klíče U2F.", + "duoDescV2": { + "message": "Zadejte kód vygenerovaný DUO Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Ověřovací kódy Vám budou zaslány e-mailem." + "emailDescV2": { + "message": "Zadejte kód odeslaný na Váš e-mail." }, "loginUnavailable": { "message": "Přihlášení není dostupné" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Potvrzení hesla souboru" }, + "exportSuccess": { + "message": "Data trezoru byla exportována" + }, "multifactorAuthenticationCancelled": { "message": "Vícefázové ověření zrušeno" }, diff --git a/apps/desktop/src/locales/cy/messages.json b/apps/desktop/src/locales/cy/messages.json index 79cd680d0f9..94f20c3e300 100644 --- a/apps/desktop/src/locales/cy/messages.json +++ b/apps/desktop/src/locales/cy/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/da/messages.json b/apps/desktop/src/locales/da/messages.json index 948e071fd99..bdb296ba717 100644 --- a/apps/desktop/src/locales/da/messages.json +++ b/apps/desktop/src/locales/da/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "De to adgangskoder matcher ikke." - }, - "newAccountCreated": { - "message": "Den nye konto er oprettet! Der kan nu logges ind." - }, "youSuccessfullyLoggedIn": { "message": "Du er nu logget ind" }, "youMayCloseThisWindow": { "message": "Dette vindue kan nu lukkes" }, + "masterPassDoesntMatch": { + "message": "De to adgangskoder matcher ikke." + }, + "newAccountCreated": { + "message": "Den nye konto er oprettet! Der kan nu logges ind." + }, "masterPassSent": { "message": "Der er sendt en e-mail til dig med dit hovedadgangskodetip." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Godkendelses-app" }, - "authenticatorAppDesc": { - "message": "Brug en godkendelses-app (såsom Authy eller Google Autenticator) til at generere tidsbaserede bekræftelseskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Angiv en kode genereret af en godkendelses-app såsom Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP-sikkerhedsnøgle" + "yubiKeyTitleV2": { + "message": "Yubico OTP-sikkerhedsnøgle" }, "yubiKeyDesc": { "message": "Brug en YubiKey for at tilgå din konto. Fungerer med YubiKey 4-, 4 Nano-, 4C- samt NEO-enheder." }, - "duoDesc": { - "message": "Bekræft med Duo Security vha. Duo Mobile-app, SMS, telefonopkald eller U2F-sikkerhedsnøgle.", + "duoDescV2": { + "message": "Angiv en kode genereret af Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Bekræftelseskoder vil blive e-mailet til dig." + "emailDescV2": { + "message": "Angiv en kode tilsendt pr. e-mail." }, "loginUnavailable": { "message": "Login utilgængelig" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Bekræft filadgangskode" }, + "exportSuccess": { + "message": "Boksdata eksporteret" + }, "multifactorAuthenticationCancelled": { "message": "Multifaktorgodkendelse afbrudt" }, diff --git a/apps/desktop/src/locales/de/messages.json b/apps/desktop/src/locales/de/messages.json index 9491415a991..ffd018bcc72 100644 --- a/apps/desktop/src/locales/de/messages.json +++ b/apps/desktop/src/locales/de/messages.json @@ -500,7 +500,7 @@ "message": "Konto erstellen" }, "setAStrongPassword": { - "message": "Ein starkes Passwort festlegen" + "message": "Lege ein starkes Passwort fest" }, "finishCreatingYourAccountBySettingAPassword": { "message": "Schließe die Erstellung deines Kontos ab, indem du ein Passwort festlegst" @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Die Passwortbestätigung stimmt nicht mit dem Passwort überein." - }, - "newAccountCreated": { - "message": "Dein neues Konto wurde erstellt! Du kannst dich jetzt anmelden." - }, "youSuccessfullyLoggedIn": { "message": "Du hast dich erfolgreich angemeldet." }, "youMayCloseThisWindow": { "message": "Du kannst dieses Fenster schließen." }, + "masterPassDoesntMatch": { + "message": "Die Passwortbestätigung stimmt nicht mit dem Passwort überein." + }, + "newAccountCreated": { + "message": "Dein neues Konto wurde erstellt! Du kannst dich jetzt anmelden." + }, "masterPassSent": { "message": "Wir haben dir eine E-Mail mit dem Master-Passwort-Hinweis gesendet." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator App" }, - "authenticatorAppDesc": { - "message": "Verwende eine Authentifizierungs-App (wie zum Beispiel Authy oder Google Authenticator), um zeitbasierte Verifizierungscodes zu generieren.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey-OTP-Sicherheitsschlüssel" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Verwende einen YubiKey, um auf dein Konto zuzugreifen. Funktioniert mit den Geräten YubiKey 4, Nano 4, 4C und NEO." }, - "duoDesc": { - "message": "Verifiziere mit Duo Security, indem du die Duo-Mobile-App, SMS, Anrufe oder U2F-Sicherheitsschlüssel benutzt.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-Mail" }, - "emailDesc": { - "message": "Bestätigungscodes werden dir per E-Mail zugesandt." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Anmeldung nicht verfügbar" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Dateipasswort bestätigen" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifaktor-Authentifizierung abgebrochen" }, diff --git a/apps/desktop/src/locales/el/messages.json b/apps/desktop/src/locales/el/messages.json index 913fffddbc3..911a3735e6d 100644 --- a/apps/desktop/src/locales/el/messages.json +++ b/apps/desktop/src/locales/el/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Η επιβεβαίωση κύριου κωδικού δεν ταιριάζει." - }, - "newAccountCreated": { - "message": "Ο λογαριασμός σας έχει δημιουργηθεί! Τώρα μπορείτε να συνδεθείτε." - }, "youSuccessfullyLoggedIn": { "message": "Έχετε συνδεθεί επιτυχώς" }, "youMayCloseThisWindow": { "message": "Μπορείτε να κλείσετε αυτό το παράθυρο" }, + "masterPassDoesntMatch": { + "message": "Η επιβεβαίωση κύριου κωδικού δεν ταιριάζει." + }, + "newAccountCreated": { + "message": "Ο λογαριασμός σας έχει δημιουργηθεί! Τώρα μπορείτε να συνδεθείτε." + }, "masterPassSent": { "message": "Σας στείλαμε ένα email με την υπόδειξη του κύριου κωδικού." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Εφαρμογή Επαλήθευσης" }, - "authenticatorAppDesc": { - "message": "Χρησιμοποιήστε μια εφαρμογή επαλήθευσης (όπως το Authy ή Google Authenticator) για να δημιουργήσετε κωδικούς επαλήθευσης με βάση το χρόνο.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Κλειδί ασφαλείας YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Χρησιμοποιήστε ένα YubiKey για να αποκτήσετε πρόσβαση στο λογαριασμό σας. Λειτουργεί με συσκευές σειράς YubiKey 4, 4 Nano, 4C και συσκευές NEO." }, - "duoDesc": { - "message": "Επαληθεύστε με το Duo Security χρησιμοποιώντας την εφαρμογή Duo Mobile, SMS, τηλεφωνική κλήση ή κλειδί ασφαλείας U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Οι κωδικοί επαλήθευσης θα σας αποσταλούν μέσω ηλεκτρονικού ταχυδρομείου." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Σύνδεση μη διαθέσιμη" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Επιβεβαίωση κωδικού πρόσβασης αρχείου" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Ο πολυμερής έλεγχος ταυτότητας ακυρώθηκε" }, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 333a1c0e7bc..51543394827 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -539,6 +539,18 @@ } } }, + "masterPassword": { + "message": "Master password" + }, + "masterPassImportant": { + "message": "Your master password cannot be recovered if you forget it!" + }, + "confirmMasterPassword": { + "message": "Confirm master password" + }, + "masterPassHintLabel": { + "message": "Master password hint" + }, "settings": { "message": "Settings" }, @@ -666,18 +678,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +705,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -1955,6 +1967,9 @@ "emailVerificationRequired": { "message": "Email verification required" }, + "emailVerifiedV2": { + "message": "Email verified" + }, "emailVerificationRequiredDesc": { "message": "You must verify your email to use this feature." }, diff --git a/apps/desktop/src/locales/en_GB/messages.json b/apps/desktop/src/locales/en_GB/messages.json index 8bcba96311f..63d9cafd136 100644 --- a/apps/desktop/src/locales/en_GB/messages.json +++ b/apps/desktop/src/locales/en_GB/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/en_IN/messages.json b/apps/desktop/src/locales/en_IN/messages.json index 47a65d56f60..6fa8989e863 100644 --- a/apps/desktop/src/locales/en_IN/messages.json +++ b/apps/desktop/src/locales/en_IN/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/eo/messages.json b/apps/desktop/src/locales/eo/messages.json index edd3b61d4d8..cbea97186fd 100644 --- a/apps/desktop/src/locales/eo/messages.json +++ b/apps/desktop/src/locales/eo/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/es/messages.json b/apps/desktop/src/locales/es/messages.json index 1c877b1c046..a0fc028f830 100644 --- a/apps/desktop/src/locales/es/messages.json +++ b/apps/desktop/src/locales/es/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "La confirmación de contraseña maestra no coincide." - }, - "newAccountCreated": { - "message": "¡Tu nueva cuenta ha sido creada! Ahora puedes acceder." - }, "youSuccessfullyLoggedIn": { "message": "Has iniciado sesión correctamente" }, "youMayCloseThisWindow": { "message": "Puedes cerrar esta ventana" }, + "masterPassDoesntMatch": { + "message": "La confirmación de contraseña maestra no coincide." + }, + "newAccountCreated": { + "message": "¡Tu nueva cuenta ha sido creada! Ahora puedes acceder." + }, "masterPassSent": { "message": "Te hemos enviado un correo electrónico con la pista de tu contraseña maestra." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplicación de autenticación" }, - "authenticatorAppDesc": { - "message": "Utiliza una aplicación de autenticación (como Authy o Google Authenticator) para generar código de verificación basados en tiempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Llave de seguridad YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Usa un Yubikey para acceder a tu cuenta. Funciona con YubiKey 4, 4 Nano, 4C y dispositivos NEO." }, - "duoDesc": { - "message": "Verificar con Duo Security usando la aplicación Duo Mobile, SMS, llamada telefónica o llave de seguridad U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Correo electrónico" }, - "emailDesc": { - "message": "Los códigos de verificación te serán enviados por correo electrónico." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Entrada no disponible" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirma la contraseña del archivo" }, + "exportSuccess": { + "message": "Datos de la caja fuerte exportados" + }, "multifactorAuthenticationCancelled": { "message": "Autenticación multifactor cancelada" }, diff --git a/apps/desktop/src/locales/et/messages.json b/apps/desktop/src/locales/et/messages.json index 43f3fc7731e..e799f8a2f40 100644 --- a/apps/desktop/src/locales/et/messages.json +++ b/apps/desktop/src/locales/et/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Ülemparoolid ei ühti." - }, - "newAccountCreated": { - "message": "Sinu konto on loodud! Võid nüüd sisse logida." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Ülemparoolid ei ühti." + }, + "newAccountCreated": { + "message": "Sinu konto on loodud! Võid nüüd sisse logida." + }, "masterPassSent": { "message": "Ülemparooli vihje saadeti Sinu e-postile." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentimise rakendus" }, - "authenticatorAppDesc": { - "message": "Kausta autentimise rakendust (näiteks Authy või Google Authenticator), et luua ajal baseeruvaid kinnituskoode.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Turvaline võti" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Kasuta kontole ligipääsemiseks YubiKey-d. See töötab YubiKey 4, 4 Nano, 4C ja NEO seadmetega." }, - "duoDesc": { - "message": "Kinnita Duo Security abil, kasutades selleks Duo Mobile rakendust, SMS-i, telefonikõnet või U2F turvavõtit.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Kinnituskoodid saadetakse e-postiga." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Sisselogimine ei ole saadaval" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/eu/messages.json b/apps/desktop/src/locales/eu/messages.json index 608e81c03a3..e7347a53cf5 100644 --- a/apps/desktop/src/locales/eu/messages.json +++ b/apps/desktop/src/locales/eu/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Pasahitz nagusiaren egiaztatzea ez dator bat." - }, - "newAccountCreated": { - "message": "Zure kontua egina dago. Orain saioa has dezakezu." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Pasahitz nagusiaren egiaztatzea ez dator bat." + }, + "newAccountCreated": { + "message": "Zure kontua egina dago. Orain saioa has dezakezu." + }, "masterPassSent": { "message": "Mezu elektroniko bat bidali dizugu zure pasahitz nagusiaren pistarekin." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentifikazio aplikazioa" }, - "authenticatorAppDesc": { - "message": "Erabili autentifikazio aplikazio bat (adibidez, Authy edo Google Authenticator) denboran oinarritutako egiaztatze-kodeak sortzeko.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP segurtasun-gakoa" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Erabili YubiKey zure kontuan sartzeko. YubiKey 4, 4 Nano, 4C eta NEO gailuekin dabil." }, - "duoDesc": { - "message": "Egiaztatu Duo Securityrekin Duo Mobile aplikazioa, SMS, telefono deia edo U2F segurtasun-gakoa erabiliz.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Emaila" }, - "emailDesc": { - "message": "Egiaztatze-kodeak email bidez bidaliko dira." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Ez dago eskuragarri saio-hasierarik" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/fa/messages.json b/apps/desktop/src/locales/fa/messages.json index 2f750dc9e61..aac7d569cb1 100644 --- a/apps/desktop/src/locales/fa/messages.json +++ b/apps/desktop/src/locales/fa/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "کلمه عبور اصلی با تکرار آن مطابقت ندارد." - }, - "newAccountCreated": { - "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." - }, "youSuccessfullyLoggedIn": { "message": "شما با موفقیت وارد شدید" }, "youMayCloseThisWindow": { "message": "می‌توانید این پنجره را ببندید" }, + "masterPassDoesntMatch": { + "message": "کلمه عبور اصلی با تکرار آن مطابقت ندارد." + }, + "newAccountCreated": { + "message": "حساب کاربری جدید شما ساخته شد! حالا می‌توانید وارد شوید." + }, "masterPassSent": { "message": "ما یک ایمیل همراه با یادآور کلمه عبور اصلی برایتان ارسال کردیم." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "برنامه احراز هویت" }, - "authenticatorAppDesc": { - "message": "از یک برنامه احراز هویت (مانند Authy یا Google Authenticator) استفاده کنید تا کدهای تأیید بر پایه زمان تولید کنید.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "کلید امنیتی YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "از یک YubiKey برای دسترسی به حسابتان استفاده کنید. همراه با دستگاه‌های YubiKey 4 ،4 Nano ،NEO کار می‌کند." }, - "duoDesc": { - "message": "با Duo Security با استفاده از برنامه تلفن همراه، پیامک، تماس تلفنی، یا کلید امنیتی U2F تأیید کنید.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "ایمیل" }, - "emailDesc": { - "message": "کد تأیید برایتان ارسال می‌شود." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "ورود به سیستم در دسترس نیست" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "تایید هویت چندمرحله‌ای کنسل شد" }, diff --git a/apps/desktop/src/locales/fi/messages.json b/apps/desktop/src/locales/fi/messages.json index f67aa2ab49d..f42aa7d127a 100644 --- a/apps/desktop/src/locales/fi/messages.json +++ b/apps/desktop/src/locales/fi/messages.json @@ -527,7 +527,7 @@ "message": "Pääsalasanan vihje (valinnainen)" }, "masterPassHintText": { - "message": "Jos unohdat salasanasi, salasanan vihje voidaan lähettää sähköpostiisi. Merkkien enimmäismäärä: $CURRENT$/$MAXIMUM$.", + "message": "Jos unohdat salasanasi, salasanavihje voidaan lähettää sähköpostiisi. Merkkien enimmäismäärä: $CURRENT$/$MAXIMUM$.", "placeholders": { "current": { "content": "$1", @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Pääsalasanan vahvistus ei täsmää." - }, - "newAccountCreated": { - "message": "Uusi käyttäjätilisi on luotu! Voit nyt kirjautua sisään." - }, "youSuccessfullyLoggedIn": { "message": "Kirjautuminen onnistui" }, "youMayCloseThisWindow": { "message": "Voit sulkea tämän ikkunan" }, + "masterPassDoesntMatch": { + "message": "Pääsalasanan vahvistus ei täsmää." + }, + "newAccountCreated": { + "message": "Uusi käyttäjätilisi on luotu! Voit nyt kirjautua sisään." + }, "masterPassSent": { "message": "Lähetimme pääsalasanasi vihjeen sinulle sähköpostitse." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Todennussovellus" }, - "authenticatorAppDesc": { - "message": "Käytä todennussovellusta (kuten Authy tai Google Authenticator) luodaksesi aikarajallisia todennuskoodeja.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Syötä Bitwarden Authenticatorin kaltaisen todennusovelluksen luoma koodi.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP -todennuslaite" + "yubiKeyTitleV2": { + "message": "Yubico OTP -todennuslaite" }, "yubiKeyDesc": { "message": "Käytä YubiKey-todennuslaitetta tilisi avaukseen. Toimii YubiKey 4, 4 Nano, 4C sekä NEO -laitteiden kanssa." }, - "duoDesc": { - "message": "Vahvista Duo Securityn avulla käyttäen Duo Mobile ‑sovellusta, tekstiviestiä, puhelua tai U2F-todennuslaitetta.", + "duoDescV2": { + "message": "Syötä Duo Securityn luoma koodi.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Sähköposti" }, - "emailDesc": { - "message": "Todennuskoodit lähetetään sinulle sähköpostitse." + "emailDescV2": { + "message": "Syötä sähköpostiisi lähetetty koodi." }, "loginUnavailable": { "message": "Kirjautuminen ei ole käytettävissä" @@ -2706,7 +2706,7 @@ "message": "Alavalikko" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Näytä/piilota sivuvalikko" }, "skipToContent": { "message": "Siirry sisältöön" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Vahvista tiedoston salasana" }, + "exportSuccess": { + "message": "Holvin tiedot on viety" + }, "multifactorAuthenticationCancelled": { "message": "Monivaiheinen todennus peruttiin" }, diff --git a/apps/desktop/src/locales/fil/messages.json b/apps/desktop/src/locales/fil/messages.json index 0f1f959e7fb..8eb371f8dcb 100644 --- a/apps/desktop/src/locales/fil/messages.json +++ b/apps/desktop/src/locales/fil/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Hindi tugma ang kumpirmasyon ng master password." - }, - "newAccountCreated": { - "message": "Nalikha na ang iyong bagong account! Maaari ka nang mag-log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Hindi tugma ang kumpirmasyon ng master password." + }, + "newAccountCreated": { + "message": "Nalikha na ang iyong bagong account! Maaari ka nang mag-log in." + }, "masterPassSent": { "message": "Pinadala na namin sa iyo ang email na may hint ng master password mo." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "App ng Authenticator" }, - "authenticatorAppDesc": { - "message": "Gumamit ng isang authenticator app (tulad ng Authy o Google Authenticator) upang makabuo ng mga code ng pag verify na batay sa oras.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP susi ng seguridad" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Gumamit ng YubiKey upang ma access ang iyong account. Gumagana sa YubiKey 4, 4 Nano, 4C, at NEO aparato." }, - "duoDesc": { - "message": "Patunayan sa Duo Security gamit ang Duo Mobile app, SMS, tawag sa telepono, o key ng seguridad ng U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Mag-email" }, - "emailDesc": { - "message": "Ang mga verification code ay i email sa iyo." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Hindi magagamit ang pag-login" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/fr/messages.json b/apps/desktop/src/locales/fr/messages.json index 9d13a216f23..817f200ce1f 100644 --- a/apps/desktop/src/locales/fr/messages.json +++ b/apps/desktop/src/locales/fr/messages.json @@ -527,7 +527,7 @@ "message": "Indice du mot de passe principal (facultatif)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Si vous oubliez votre mot de passe, l'indice peut être envoyé à votre courriel. $CURRENT$/$MAXIMUM$ caractères maximum.", "placeholders": { "current": { "content": "$1", @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "La confirmation du mot de passe principal ne correspond pas." - }, - "newAccountCreated": { - "message": "Votre nouveau compte a été créé ! Vous pouvez maintenant vous authentifier." - }, "youSuccessfullyLoggedIn": { "message": "Vous vous êtes connecté avec succès" }, "youMayCloseThisWindow": { "message": "Vous pouvez fermer cette fenêtre" }, + "masterPassDoesntMatch": { + "message": "La confirmation du mot de passe principal ne correspond pas." + }, + "newAccountCreated": { + "message": "Votre nouveau compte a été créé ! Vous pouvez maintenant vous authentifier." + }, "masterPassSent": { "message": "Nous vous avons envoyé un courriel avec votre indice de mot de passe principal." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Application d'authentification" }, - "authenticatorAppDesc": { - "message": "Utiliser une application d'authentification (comme Authy ou Google Authenticator) pour générer des codes de vérification basés sur le temps.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Clé de sécurité YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Utiliser une YubiKey pour accéder à votre compte. Fonctionne avec les appareils YubiKey 4, 4 Nano, 4C et NEO." }, - "duoDesc": { - "message": "S'authentifier avec Duo Security via l'application Duo Mobile, un SMS, un appel téléphonique, ou une clé de sécurité U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Courriel" }, - "emailDesc": { - "message": "Les codes de vérification vous seront envoyés par courriel." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Identifiant non disponible" @@ -1337,7 +1337,7 @@ "description": "ex. Date this password was updated" }, "exportFrom": { - "message": "Export from" + "message": "Exporter depuis" }, "exportVault": { "message": "Exporter le coffre" @@ -1364,13 +1364,13 @@ "message": "Définissez un mot de passe de fichier pour chiffrer l'export et déchiffrer son import sur n'importe quel compte Bitwarden." }, "exportTypeHeading": { - "message": "Export type" + "message": "Type d'exportation" }, "accountRestricted": { - "message": "Account restricted" + "message": "Compte restreint" }, "filePasswordAndConfirmFilePasswordDoNotMatch": { - "message": "“File password” and “Confirm file password“ do not match." + "message": "Le \"Mot de passe du fichier\" et la \"Confirmation du mot de passe du fichier\" ne correspondent pas." }, "hCaptchaUrl": { "message": "URL hCaptcha", @@ -1679,19 +1679,19 @@ "message": "Votre nouveau mot de passe principal ne répond pas aux exigences de politique de sécurité." }, "receiveMarketingEmails": { - "message": "Get emails from Bitwarden for announcements, advice, and research opportunities." + "message": "Recevez des courriels de Bitwarden pour des annonces, des conseils et des opportunités de recherche." }, "unsubscribe": { - "message": "Unsubscribe" + "message": "Se désabonner" }, "atAnyTime": { - "message": "at any time." + "message": "à tout moment." }, "byContinuingYouAgreeToThe": { - "message": "By continuing, you agree to the" + "message": "En continuant, vous acceptez les" }, "and": { - "message": "and" + "message": "et" }, "acceptPolicies": { "message": "En cochant cette case vous acceptez ce qui suit :" @@ -2133,7 +2133,7 @@ "message": "Changer de compte" }, "alreadyHaveAccount": { - "message": "Already have an account?" + "message": "Vous avez déjà un compte ?" }, "options": { "message": "Options" @@ -2154,7 +2154,7 @@ } }, "exportingOrganizationVaultTitle": { - "message": "Exporting organization vault" + "message": "Export du coffre-fort de l'organisation" }, "exportingOrganizationVaultDesc": { "message": "Only the organization vault associated with $ORGANIZATION$ will be exported. Items in individual vaults or other organizations will not be included.", @@ -2244,11 +2244,11 @@ } }, "forwarderGeneratedBy": { - "message": "Generated by Bitwarden.", + "message": "Généré par Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen." }, "forwarderGeneratedByWithWebsite": { - "message": "Website: $WEBSITE$. Generated by Bitwarden.", + "message": "Site web : $WEBSITE$. Généré par Bitwarden.", "description": "Displayed with the address on the forwarding service's configuration screen.", "placeholders": { "WEBSITE": { @@ -2292,7 +2292,7 @@ } }, "forwarderNoDomain": { - "message": "Invalid $SERVICENAME$ domain.", + "message": "Le domaine $SERVICENAME$ est invalide.", "description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.", "placeholders": { "servicename": { @@ -2405,7 +2405,7 @@ "message": "Renvoyer la notification" }, "toggleCharacterCount": { - "message": "Activer/Désactiver le compte de caractères", + "message": "Activer / désactiver le compte de caractères", "description": "'Character count' describes a feature that displays a number next to each character of the password." }, "areYouTryingtoLogin": { @@ -2482,25 +2482,25 @@ "message": "Connexion demandée" }, "creatingAccountOn": { - "message": "Creating account on" + "message": "Création du compte sur" }, "checkYourEmail": { - "message": "Check your email" + "message": "Vérifiez votre courriel" }, "followTheLinkInTheEmailSentTo": { - "message": "Follow the link in the email sent to" + "message": "Suivez le lien dans le courriel envoyé à" }, "andContinueCreatingYourAccount": { - "message": "and continue creating your account." + "message": "et continuer à créer votre compte." }, "noEmail": { - "message": "No email?" + "message": "Pas de courriel ?" }, "goBack": { - "message": "Go back" + "message": "Revenir en arrière" }, "toEditYourEmailAddress": { - "message": "to edit your email address." + "message": "pour modifier votre courriel." }, "exposedMasterPassword": { "message": "Mot de passe principal exposé" @@ -2521,10 +2521,10 @@ "message": "Important :" }, "accessTokenUnableToBeDecrypted": { - "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." + "message": "Vous avez été déconnecté car votre jeton d'accès n'a pas pu être déchiffré. Veuillez vous reconnecter pour résoudre ce problème." }, "refreshTokenSecureStorageRetrievalFailure": { - "message": "You have been logged out because your refresh token could not be retrieved. Please log in again to resolve this issue." + "message": "Vous avez été déconnecté car votre jeton d'accès n'a pas pu être récupéré. Veuillez vous reconnecter pour résoudre ce problème." }, "masterPasswordHint": { "message": "Votre mot de passe principal ne peut pas être récupéré si vous l'oubliez !" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirmez le mot de passe du fichier" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Authentification multi-facteurs annulée" }, @@ -2965,11 +2968,11 @@ } }, "back": { - "message": "Back", + "message": "Retour", "description": "Button text to navigate back" }, "removeItem": { - "message": "Remove $NAME$", + "message": "Supprimer $NAME$", "description": "Remove a selected option, such as a folder or collection", "placeholders": { "name": { diff --git a/apps/desktop/src/locales/gl/messages.json b/apps/desktop/src/locales/gl/messages.json index 767619b10f4..0887d769823 100644 --- a/apps/desktop/src/locales/gl/messages.json +++ b/apps/desktop/src/locales/gl/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/he/messages.json b/apps/desktop/src/locales/he/messages.json index 1df2b148732..8d82180f1de 100644 --- a/apps/desktop/src/locales/he/messages.json +++ b/apps/desktop/src/locales/he/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "אימות סיסמה ראשית אינו תואם." - }, - "newAccountCreated": { - "message": "החשבון החדש שלך נוצר בהצלחה! כעת ניתן להתחבר למערכת." - }, "youSuccessfullyLoggedIn": { "message": "נכנסת בהצלחה" }, "youMayCloseThisWindow": { "message": "אפשר לסגור את החלון הזה" }, + "masterPassDoesntMatch": { + "message": "אימות סיסמה ראשית אינו תואם." + }, + "newAccountCreated": { + "message": "החשבון החדש שלך נוצר בהצלחה! כעת ניתן להתחבר למערכת." + }, "masterPassSent": { "message": "שלחנו לך אימייל עם רמז לסיסמה הראשית." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "אפליקציית אימות" }, - "authenticatorAppDesc": { - "message": "השתמש באפליקצית אימות (כמו לדוגמא Authy או Google Authenticator) לייצור סיסמאות אימות מבוססות זמן.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "מפתח אבטחה OTP של YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "השתמש בYubiKey עבור גישה לחשבון שלך. עובד עם YubiKey בגירסאות 4, 4C, 4Nano, ומכשירי NEO." }, - "duoDesc": { - "message": "בצע אימות מול Duo Security באמצעות אפליקצית Duo לפלאפון, SMS, שיחת טלפון, או מפתח אבטחה U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "אימייל" }, - "emailDesc": { - "message": "קודים לאימות יישלחו אליך באימייל." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "פרטי כניסה לא זמינים" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/hi/messages.json b/apps/desktop/src/locales/hi/messages.json index 8a49a38899a..78367ecd0dd 100644 --- a/apps/desktop/src/locales/hi/messages.json +++ b/apps/desktop/src/locales/hi/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/hr/messages.json b/apps/desktop/src/locales/hr/messages.json index c810085b128..6d26e9c3c6b 100644 --- a/apps/desktop/src/locales/hr/messages.json +++ b/apps/desktop/src/locales/hr/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Potvrda glavne lozinke se ne podudara." - }, - "newAccountCreated": { - "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Potvrda glavne lozinke se ne podudara." + }, + "newAccountCreated": { + "message": "Tvoj novi račun je kreiran! Sada se možeš prijaviti." + }, "masterPassSent": { "message": "Poslali smo e-poštu s podsjetnikom glavne lozinke." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentifikatorska aplikacija" }, - "authenticatorAppDesc": { - "message": "Koristi autentifikatorsku aplikaciju (npr. Authy ili Google Authentifikator) za generiranje kontrolnih kodova.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP sigurnosni ključ" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Koristi YubiKey za pristup svojem računu. Radi s YubiKey 4, 4 Nano, 4C i NEO uređajima." }, - "duoDesc": { - "message": "Potvrdi s Duo Security pomoću aplikacije Duo Mobile, SMS-om, telefonskim pozivom ili U2F sigurnosnim ključem.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-pošta" }, - "emailDesc": { - "message": "Verifikacijski kodovi će biti poslani e-poštom." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Prijava nije dostupna" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Potvrdi lozinku datoteke" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifaktorska autentifikacija otkazana" }, diff --git a/apps/desktop/src/locales/hu/messages.json b/apps/desktop/src/locales/hu/messages.json index 957eba8a95e..14c97b81fe5 100644 --- a/apps/desktop/src/locales/hu/messages.json +++ b/apps/desktop/src/locales/hu/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "A mesterjelszó megerősítése nem egyezik." - }, - "newAccountCreated": { - "message": "A fiók létrehozásra került. Most már be lehet jelentkezni." - }, "youSuccessfullyLoggedIn": { "message": "A bejelentkezés sikeres volt." }, "youMayCloseThisWindow": { "message": "Most már bezárható ez az ablak." }, + "masterPassDoesntMatch": { + "message": "A mesterjelszó megerősítése nem egyezik." + }, + "newAccountCreated": { + "message": "A fiók létrehozásra került. Most már be lehet jelentkezni." + }, "masterPassSent": { "message": "Elküldtünk neked egy emailt a mesterjelszó emlékeztetővel." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Hitelesítő alkalmazás" }, - "authenticatorAppDesc": { - "message": "Hitelesítő alkalmazás használata (mint például az Authy vagy a Google Authenticator) idő alapú ellenőrzőkód generálásához.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Adjunk meg egy hitelesítő alkalmazás, például a Bitwarden Authenticator által generált kódot.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP egyszeri időalapú jelszó biztonsági kulcs" + "yubiKeyTitleV2": { + "message": "YubiKey OTP biztonsági kulcs" }, "yubiKeyDesc": { "message": "YubiKey használata a fiók eléréséhez. Működik a YubiKey 4, 4 Nano, 4C, és NEO eszközökkel." }, - "duoDesc": { - "message": "Ellenőrzés Duo Security segítségével a Duo Mobile alkalmazás, SMS, telefonhívás vagy U2F biztonsági kulcs használatával.", + "duoDescV2": { + "message": "Adjuk meg a Duo Security által generált kódot.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email cím" }, - "emailDesc": { - "message": "Az ellenőrző kódok emailben kerülnek elküldésre." + "emailDescV2": { + "message": "Adjuk meg az email címre elküldött kódot." }, "loginUnavailable": { "message": "A bejelentkezés nem érhető el." @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Fájl jelszó megerősítés" }, + "exportSuccess": { + "message": "A széfadatok exportálásra kerültek." + }, "multifactorAuthenticationCancelled": { "message": "A többtényezős hitelesítés megszakításra került." }, diff --git a/apps/desktop/src/locales/id/messages.json b/apps/desktop/src/locales/id/messages.json index f479704bf48..574ab5bb6b2 100644 --- a/apps/desktop/src/locales/id/messages.json +++ b/apps/desktop/src/locales/id/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Konfirmasi sandi utama tidak cocok." - }, - "newAccountCreated": { - "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." - }, "youSuccessfullyLoggedIn": { "message": "Anda berhasil masuk" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Konfirmasi sandi utama tidak cocok." + }, + "newAccountCreated": { + "message": "Akun baru Anda telah dibuat! Sekarang Anda bisa masuk." + }, "masterPassSent": { "message": "Kami telah mengirimi Anda email dengan petunjuk sandi utama Anda." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplikasi Autentikasi" }, - "authenticatorAppDesc": { - "message": "Gunakan aplikasi autentikasi (seperti Authy atau Google Authenticator) untuk menghasilkan kode verifikasi berbasis waktu.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Kunci Keamanan OTP YubiKey" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Gunakan YubiKey untuk mengakses akun Anda. Bekerja dengan YubiKey 4, 4 Nano, 4C, dan peranti NEO." }, - "duoDesc": { - "message": "Verifikasi dengan Duo Security menggunakan aplikasi Duo Mobile, SMS, panggilan telepon, atau kunci keamanan U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Kode verifikasi akan dikirim via email kepada Anda." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Info Masuk Tidak Tersedia" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/it/messages.json b/apps/desktop/src/locales/it/messages.json index d7066a9e60f..c0c1e322c65 100644 --- a/apps/desktop/src/locales/it/messages.json +++ b/apps/desktop/src/locales/it/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Le password principali non corrispondono." - }, - "newAccountCreated": { - "message": "Il tuo nuovo account è stato creato! Ora puoi eseguire l'accesso." - }, "youSuccessfullyLoggedIn": { "message": "Hai effettuato l'accesso" }, "youMayCloseThisWindow": { "message": "Puoi chiudere questa finestra" }, + "masterPassDoesntMatch": { + "message": "Le password principali non corrispondono." + }, + "newAccountCreated": { + "message": "Il tuo nuovo account è stato creato! Ora puoi eseguire l'accesso." + }, "masterPassSent": { "message": "Ti abbiamo inviato un'email con il tuo suggerimento per la password principale." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "App di autenticazione" }, - "authenticatorAppDesc": { - "message": "Usa un'app di autenticazione (come Authy o Google Authenticator) per generare codici di verifica a tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chiave di sicurezza YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Usa YubiKey per accedere al tuo account. Compatibile con YubiKey 4, 4 Nano, 4C, e dispositivi NEO." }, - "duoDesc": { - "message": "Verifica con Duo Security usando l'app Duo Mobile, SMS, chiamata telefonica, o chiave di sicurezza U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "I codici di verifica ti saranno inviati per email." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login non disponibile" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Conferma password del file" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Verifica in due passaggi annullata" }, diff --git a/apps/desktop/src/locales/ja/messages.json b/apps/desktop/src/locales/ja/messages.json index 96e23984e6b..80060295411 100644 --- a/apps/desktop/src/locales/ja/messages.json +++ b/apps/desktop/src/locales/ja/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "マスターパスワードが一致しません。" - }, - "newAccountCreated": { - "message": "新しいアカウントを作成しました!今すぐログインできます。" - }, "youSuccessfullyLoggedIn": { "message": "ログインに成功しました" }, "youMayCloseThisWindow": { "message": "ウィンドウを閉じて大丈夫です" }, + "masterPassDoesntMatch": { + "message": "マスターパスワードが一致しません。" + }, + "newAccountCreated": { + "message": "新しいアカウントを作成しました!今すぐログインできます。" + }, "masterPassSent": { "message": "あなたのマスターパスワードのヒントを記載したメールを送信しました。" }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "認証アプリ" }, - "authenticatorAppDesc": { - "message": "Authy や Google 認証システムなどの認証アプリで時限式の認証コードを生成してください。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Bitwarden Authenticator のような認証アプリによって生成されたコードを入力してください。", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP セキュリティキー" + "yubiKeyTitleV2": { + "message": "Yubico OTP セキュリティキー" }, "yubiKeyDesc": { "message": "YubiKey を使ってアカウントにアクセスできます。 YubiKey 4、4 Nano、4C、NEOに対応しています。" }, - "duoDesc": { - "message": "Duo Mobile アプリや SMS、電話や U2F セキュリティキーを使って Duo Security で認証します。", + "duoDescV2": { + "message": "Duo Security によって生成されたコードを入力してください。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "メールアドレス" }, - "emailDesc": { - "message": "確認コードをメールにお送りします。" + "emailDescV2": { + "message": "メールアドレスに送信されたコードを入力してください。" }, "loginUnavailable": { "message": "ログインできません。" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "ファイルパスワードの確認" }, + "exportSuccess": { + "message": "保管庫データをエクスポートしました" + }, "multifactorAuthenticationCancelled": { "message": "多要素認証がキャンセルされました" }, diff --git a/apps/desktop/src/locales/ka/messages.json b/apps/desktop/src/locales/ka/messages.json index 767619b10f4..0887d769823 100644 --- a/apps/desktop/src/locales/ka/messages.json +++ b/apps/desktop/src/locales/ka/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/km/messages.json b/apps/desktop/src/locales/km/messages.json index 767619b10f4..0887d769823 100644 --- a/apps/desktop/src/locales/km/messages.json +++ b/apps/desktop/src/locales/km/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/kn/messages.json b/apps/desktop/src/locales/kn/messages.json index 5f2b2411577..ae4d9150af5 100644 --- a/apps/desktop/src/locales/kn/messages.json +++ b/apps/desktop/src/locales/kn/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ದೃಢೀಕರಣವು ಹೊಂದಿಕೆಯಾಗುವುದಿಲ್ಲ." - }, - "newAccountCreated": { - "message": "ನಿಮ್ಮ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಲಾಗಿದೆ! ನೀವು ಈಗ ಲಾಗ್ ಇನ್ ಮಾಡಬಹುದು." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ದೃಢೀಕರಣವು ಹೊಂದಿಕೆಯಾಗುವುದಿಲ್ಲ." + }, + "newAccountCreated": { + "message": "ನಿಮ್ಮ ಹೊಸ ಖಾತೆಯನ್ನು ರಚಿಸಲಾಗಿದೆ! ನೀವು ಈಗ ಲಾಗ್ ಇನ್ ಮಾಡಬಹುದು." + }, "masterPassSent": { "message": "ನಿಮ್ಮ ಮಾಸ್ಟರ್ ಪಾಸ್‌ವರ್ಡ್ ಸುಳಿವಿನೊಂದಿಗೆ ನಾವು ನಿಮಗೆ ಇಮೇಲ್ ಕಳುಹಿಸಿದ್ದೇವೆ." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್" }, - "authenticatorAppDesc": { - "message": "ಸಮಯ ಆಧಾರಿತ ಪರಿಶೀಲನಾ ಕೋಡ್‌ಗಳನ್ನು ರಚಿಸಲು ದೃಢೀಕರಣ ಅಪ್ಲಿಕೇಶನ್ ಅನ್ನು ಬಳಸಿ (ಆಥಿ ಅಥವಾ ಗೂಗಲ್ ಅಥೆಂಟಿಕೇಟರ್).", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "ಯುಬಿಕೆ ಒಟಿಪಿ ಭದ್ರತಾ ಕೀ" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "ನಿಮ್ಮ ಖಾತೆಯನ್ನು ಪ್ರವೇಶಿಸಲು ಯುಬಿಕೆ ಬಳಸಿ. ಯುಬಿಕೆ 4, 4 ನ್ಯಾನೋ, 4 ಸಿ ಮತ್ತು ಎನ್ಇಒ ಸಾಧನಗಳೊಂದಿಗೆ ಕಾರ್ಯನಿರ್ವಹಿಸುತ್ತದೆ." }, - "duoDesc": { - "message": "ಡ್ಯುಯೊ ಮೊಬೈಲ್ ಅಪ್ಲಿಕೇಶನ್, ಎಸ್‌ಎಂಎಸ್, ಫೋನ್ ಕರೆ ಅಥವಾ ಯು 2 ಎಫ್ ಭದ್ರತಾ ಕೀಲಿಯನ್ನು ಬಳಸಿಕೊಂಡು ಡ್ಯುಯೊ ಸೆಕ್ಯುರಿಟಿಯೊಂದಿಗೆ ಪರಿಶೀಲಿಸಿ.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "ಇಮೇಲ್" }, - "emailDesc": { - "message": "ಪರಿಶೀಲನೆ ಕೋಡ್‌ಗಳನ್ನು ನಿಮಗೆ ಇಮೇಲ್ ಮಾಡಲಾಗುತ್ತದೆ." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "ಲಾಗಿನ್ ಲಭ್ಯವಿಲ್ಲ" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/ko/messages.json b/apps/desktop/src/locales/ko/messages.json index 64ec0f45359..63d03f5ebf9 100644 --- a/apps/desktop/src/locales/ko/messages.json +++ b/apps/desktop/src/locales/ko/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "마스터 비밀번호 확인과 마스터 비밀번호가 일치하지 않습니다." - }, - "newAccountCreated": { - "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." - }, "youSuccessfullyLoggedIn": { "message": "로그인에 성공했습니다." }, "youMayCloseThisWindow": { "message": "이제 창을 닫으실 수 있습니다." }, + "masterPassDoesntMatch": { + "message": "마스터 비밀번호 확인과 마스터 비밀번호가 일치하지 않습니다." + }, + "newAccountCreated": { + "message": "계정 생성이 완료되었습니다! 이제 로그인하실 수 있습니다." + }, "masterPassSent": { "message": "마스터 비밀번호 힌트가 담긴 이메일을 보냈습니다." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "인증 앱" }, - "authenticatorAppDesc": { - "message": "인증 앱(Authy, Google OTP 등)을 통하여 일회용 인증 코드를 생성합니다.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 보안 키" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "YubiKey를 사용하여 사용자의 계정에 접근합니다. YubiKey 4, 4 Nano, 4C 및 NEO 기기를 사용할 수 있습니다." }, - "duoDesc": { - "message": "Duo Mobile 앱, SMS, 전화 통화를 사용한 Duo Security 또는 U2F 보안 키를 사용하여 인증하세요.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "이메일" }, - "emailDesc": { - "message": "인증 코드가 담긴 이메일을 다시 보냅니다." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "로그인 불가능" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/lt/messages.json b/apps/desktop/src/locales/lt/messages.json index b0b6405cedb..e1020f7d237 100644 --- a/apps/desktop/src/locales/lt/messages.json +++ b/apps/desktop/src/locales/lt/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Pagrindinio slaptažodžio patvirtinimas nesutampa." - }, - "newAccountCreated": { - "message": "Jūsų paskyra sukurta! Galite prisijungti." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Pagrindinio slaptažodžio patvirtinimas nesutampa." + }, + "newAccountCreated": { + "message": "Jūsų paskyra sukurta! Galite prisijungti." + }, "masterPassSent": { "message": "Išsiuntėme jums el. laišką su pagrindinio slaptažodžio užuomina." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentifikavimo programa" }, - "authenticatorAppDesc": { - "message": "Naudokite autentifikatoriaus programėlę (pvz. Authy arba Google Authenticator), kad sugeneruotumėte laiko patikrinimo kodus.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP saugumo raktas" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Naudokite YubiKey, kad prisijungtumėte prie savo paskyros. Veikia su YubiKey 4, 4 Nano, 4C ir NEO įrenginiais." }, - "duoDesc": { - "message": "Patvirtinkite su Duo Security naudodami Duo Mobile programą, SMS žinutę, telefono skambutį arba U2F saugumo raktą.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "El. paštas" }, - "emailDesc": { - "message": "Patvirtinimo kodai bus atsiųsti el. paštu." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Prisijungimas nepasiekiamas" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Patvirtinti failo slaptažodį" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Kelių veiksnių autentifikacija atšaukta" }, diff --git a/apps/desktop/src/locales/lv/messages.json b/apps/desktop/src/locales/lv/messages.json index 6e0dcb30d4e..64c27f9f627 100644 --- a/apps/desktop/src/locales/lv/messages.json +++ b/apps/desktop/src/locales/lv/messages.json @@ -527,7 +527,7 @@ "message": "Galvenās paroles norāde (nav nepieciešama)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "Ja tiks aizmirsta parole, tās norādi var nosūtīt uz e-pasta adresi. $CURRENT$/$MAXIMUM$ lielākais pieļaujamais rakstzīmju skaits.", "placeholders": { "current": { "content": "$1", @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Galvenās paroles apstiprinājums nesakrīt." - }, - "newAccountCreated": { - "message": "Jaunais konts ir izveidots. Tagad vari pieteikties." - }, "youSuccessfullyLoggedIn": { "message": "Pieteikšanās bija veiksmīga" }, "youMayCloseThisWindow": { "message": "Šo logu var aizvērt" }, + "masterPassDoesntMatch": { + "message": "Galvenās paroles apstiprinājums nesakrīt." + }, + "newAccountCreated": { + "message": "Jaunais konts ir izveidots. Tagad vari pieteikties." + }, "masterPassSent": { "message": "Mēs nosūtījām galvenās paroles norādi e-pastā." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentificētāja lietotne" }, - "authenticatorAppDesc": { - "message": "Izmanto autentificētāja lietotni (piemēram, Authy vai Google autentifikators), lai izveidotu laikā balstītus apstiprinājuma kodus!", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Jāievada autentificētāja lietotnes, piemēram, Bitwarden Authenticator, izveidots kods.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP drošības atslēga" + "yubiKeyTitleV2": { + "message": "Yubico OTP drošības atslēga" }, "yubiKeyDesc": { "message": "Ir izmantojams YubiKey, lai piekļūtu savam kontam. Darbojas ar YubiKey 4, 4 Nano, 4C un NEO ierīcēm." }, - "duoDesc": { - "message": "Ar Duo Security apliecināšanu var veikt ar Duo Mobile lietotni, īsziņu, tālruņa zvanu vai U2F drošības atslēgu.", + "duoDescV2": { + "message": "Jāievada Duo Security izveidots kods.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-pasts" }, - "emailDesc": { - "message": "Apstiprinājuma kodi tiks nosūtīti e-pastā." + "emailDescV2": { + "message": "Jāievada e-pastā nosūtītais kods." }, "loginUnavailable": { "message": "Pieteikšanās nav pieejama" @@ -861,7 +861,7 @@ "message": "Sinhronizācija neizdevās" }, "yourVaultIsLocked": { - "message": "Glabātava ir aizslēgta. Jāapstiprina identitāte, lai turpinātu." + "message": "Glabātava ir aizslēgta. Jāapliecina sava identitāte, lai turpinātu." }, "unlock": { "message": "Atslēgt" @@ -1965,7 +1965,7 @@ "message": "Galvenās paroles apstiprināšana" }, "passwordConfirmationDesc": { - "message": "Šī darbība ir aizsargāta. Lai turpinātu, ir jāievada galvenā parole, lai apstiprinātu identitāti." + "message": "Šī darbība ir aizsargāta. Lai turpinātu, ir jāievada galvenā parole, lai apliecinātu savu identitāti." }, "updatedMasterPassword": { "message": "Galvenā parole atjaunināta" @@ -2706,7 +2706,7 @@ "message": "Apakšizvēlne" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "Pārslēgt sānu pārvietošanās joslu" }, "skipToContent": { "message": "Pāriet uz saturu" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Apstiprināt datnes paroli" }, + "exportSuccess": { + "message": "Glabātavas saturs izgūts" + }, "multifactorAuthenticationCancelled": { "message": "Daudzpakāpju pieteikšanās atcelta" }, diff --git a/apps/desktop/src/locales/me/messages.json b/apps/desktop/src/locales/me/messages.json index fcfc3875a6a..6e02878b12d 100644 --- a/apps/desktop/src/locales/me/messages.json +++ b/apps/desktop/src/locales/me/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Potvrda glavne lozinke ne odgovara." - }, - "newAccountCreated": { - "message": "Vaš novi nalog je kreiran! Sada se možete prijaviti." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Potvrda glavne lozinke ne odgovara." + }, + "newAccountCreated": { + "message": "Vaš novi nalog je kreiran! Sada se možete prijaviti." + }, "masterPassSent": { "message": "Poslali smo vam email sa podsjetnikom na glavnu lozinku." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplikacija za autentifikaciju" }, - "authenticatorAppDesc": { - "message": "Koristi aplikaciju za autentifkaciju (kao što su Authy ili Google autentificator) da generišeš verfikacione kodove bazirane na vremenu.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP siguronosni ključ" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Koristi YubiKey da pristupis svom nalogu. Radi sa YubiKey 4, 4 Nano, 4C i NEO uređajima." }, - "duoDesc": { - "message": "Potvrdite sa Duo Security, korišćenjem Duo Mobile aplikacije, SMS-a, telefonskog poziva ili U2F sigurnosnog ključa.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verifikacioni kodovi će vam biti poslati na email adresu." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Prijava nije dostupna" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/ml/messages.json b/apps/desktop/src/locales/ml/messages.json index de4fd5ad8cb..880cc5b0848 100644 --- a/apps/desktop/src/locales/ml/messages.json +++ b/apps/desktop/src/locales/ml/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "പ്രാഥമിക പാസ്‌വേഡ് സ്ഥിരീകരണം പൊരുത്തപ്പെടുന്നില്ല." - }, - "newAccountCreated": { - "message": "തങ്ങളുടെ അക്കൗണ്ട് സൃഷ്ടിക്കപ്പെട്ടു. ഇനി താങ്കൾക്ക് ലോഗിൻ ചെയ്യാം." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "പ്രാഥമിക പാസ്‌വേഡ് സ്ഥിരീകരണം പൊരുത്തപ്പെടുന്നില്ല." + }, + "newAccountCreated": { + "message": "തങ്ങളുടെ അക്കൗണ്ട് സൃഷ്ടിക്കപ്പെട്ടു. ഇനി താങ്കൾക്ക് ലോഗിൻ ചെയ്യാം." + }, "masterPassSent": { "message": "നിങ്ങളുടെ പ്രാഥമിക പാസ്‌വേഡ് സൂചനയുള്ള ഒരു ഇമെയിൽ ഞങ്ങൾ നിങ്ങൾക്ക് അയച്ചു." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "ഓതന്റിക്കേറ്റർ ആപ്പ്" }, - "authenticatorAppDesc": { - "message": "സമയ-അടിസ്ഥാന പരിശോധന കോഡുകൾ സൃഷ്ടിക്കുന്നതിന് ഒരു ഓതന്റിക്കേറ്റർ അപ്ലിക്കേഷൻ (ഓത്തി അല്ലെങ്കിൽ Google ഓതന്റിക്കേറ്റർ പോലുള്ളവ) ഉപയോഗിക്കുക.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP സുരക്ഷാ കീ" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "നിങ്ങളുടെ അക്കൗണ്ട് ആക്സസ് ചെയ്യുന്നതിന് ഒരു യൂബിക്കി ഉപയോഗിക്കുക. YubiKey 4, 4 Nano, 4C, NEO ഉപകരണങ്ങളിൽ പ്രവർത്തിക്കുന്നു." }, - "duoDesc": { - "message": "Duo Mobile അപ്ലിക്കേഷൻ, എസ്എംഎസ്, ഫോൺ കോൾ അല്ലെങ്കിൽ യു 2 എഫ് സുരക്ഷാ കീ ഉപയോഗിച്ച് Duoസെക്യൂരിറ്റി ഉപയോഗിച്ച് പരിശോധിക്കുക.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "ഇമെയിൽ" }, - "emailDesc": { - "message": "സ്ഥിരീകരണ കോഡുകൾ നിങ്ങൾക്ക് ഇമെയിൽ ചെയ്യും." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "പ്രവേശനം ലഭ്യമല്ല" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/mr/messages.json b/apps/desktop/src/locales/mr/messages.json index 767619b10f4..0887d769823 100644 --- a/apps/desktop/src/locales/mr/messages.json +++ b/apps/desktop/src/locales/mr/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/my/messages.json b/apps/desktop/src/locales/my/messages.json index ff0caa06d31..d007a4e0f45 100644 --- a/apps/desktop/src/locales/my/messages.json +++ b/apps/desktop/src/locales/my/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/nb/messages.json b/apps/desktop/src/locales/nb/messages.json index de12803afe4..03d6c95a293 100644 --- a/apps/desktop/src/locales/nb/messages.json +++ b/apps/desktop/src/locales/nb/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Superpassord-bekreftelsen er ikke samsvarende." - }, - "newAccountCreated": { - "message": "Din nye konto har blitt opprettet! Du kan nå logge på." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Superpassord-bekreftelsen er ikke samsvarende." + }, + "newAccountCreated": { + "message": "Din nye konto har blitt opprettet! Du kan nå logge på." + }, "masterPassSent": { "message": "Vi har sendt deg en E-post med hintet til superpassordet." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentiseringsapp" }, - "authenticatorAppDesc": { - "message": "Bruk en autentiseringsapp (f․eks․ Authy eller Google Authenticator) for å generere tidsbegrensede verifiseringskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP-sikkerhetsnøkkel" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Bruk en YubiKey for å få tilgang til kontoen din. Virker med enheter av typene YubiKey 4, 4 Nano, 4C, og NEO." }, - "duoDesc": { - "message": "Verifiser med Duo Security gjennom Duo Mobile-appen, SMS, telefonsamtale, eller en U2F-sikkerhetsnøkkel.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Verifiseringskoder vil bli sendt til deg med E-post." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Innloggingen er utilgjengelig" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Bekreft filpassord" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifaktorautentisering ble avbrutt" }, diff --git a/apps/desktop/src/locales/ne/messages.json b/apps/desktop/src/locales/ne/messages.json index f7057c36ff2..560a5c41c70 100644 --- a/apps/desktop/src/locales/ne/messages.json +++ b/apps/desktop/src/locales/ne/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/nl/messages.json b/apps/desktop/src/locales/nl/messages.json index a0661e3a4aa..796b23b0c36 100644 --- a/apps/desktop/src/locales/nl/messages.json +++ b/apps/desktop/src/locales/nl/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "De hoofdwachtwoorden komen niet overeen." - }, - "newAccountCreated": { - "message": "Je nieuwe account is aangemaakt! Je kunt nu inloggen." - }, "youSuccessfullyLoggedIn": { "message": "Je bent succesvol ingelogd" }, "youMayCloseThisWindow": { "message": "Je kunt dit venster sluiten" }, + "masterPassDoesntMatch": { + "message": "De hoofdwachtwoorden komen niet overeen." + }, + "newAccountCreated": { + "message": "Je nieuwe account is aangemaakt! Je kunt nu inloggen." + }, "masterPassSent": { "message": "We hebben je een e-mail gestuurd met je hoofdwachtwoordhint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticatie-app" }, - "authenticatorAppDesc": { - "message": "Gebruik een authenticatie-app (zoals Authy of Google Authenticator) om tijdgebaseerde authenticatiecodes te genereren.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Door een authenticatie-app zoals Bitwarden Authenticator gegenereerde code invoeren.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { + "yubiKeyTitleV2": { "message": "YubiKey OTP-beveiligingssleutel" }, "yubiKeyDesc": { "message": "Gebruik een YubiKey om toegang te krijgen tot je account. Werkt met YubiKey 4, 4 Nano, 4C en Neo-apparaten." }, - "duoDesc": { - "message": "Verificatie met Duo Security middels de Duo Mobile-app, sms, spraakoproep of een U2F-beveiligingssleutel.", + "duoDescV2": { + "message": "Door Duo Security gegenereerde code invoeren.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Je ontvangt verificatiecodes via e-mail." + "emailDescV2": { + "message": "Via e-mail verstuurde code invoeren." }, "loginUnavailable": { "message": "Login niet beschikbaar" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Bestandswachtwoord bevestigen" }, + "exportSuccess": { + "message": "Kluisgegevens geëxporteerd" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor-authenticatie geannuleerd" }, diff --git a/apps/desktop/src/locales/nn/messages.json b/apps/desktop/src/locales/nn/messages.json index 4290e95103a..f6d4a6ed3c4 100644 --- a/apps/desktop/src/locales/nn/messages.json +++ b/apps/desktop/src/locales/nn/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Stadfesting av hovudpassordet samsvarar ikkje." - }, - "newAccountCreated": { - "message": "Den nye kontoen din har blitt oppretta! Du kan no logge inn." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Stadfesting av hovudpassordet samsvarar ikkje." + }, + "newAccountCreated": { + "message": "Den nye kontoen din har blitt oppretta! Du kan no logge inn." + }, "masterPassSent": { "message": "Me har sendt deg ein e-post med eit hint om hovudpassordet ditt." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Innlogginga er utilgjengeleg" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/or/messages.json b/apps/desktop/src/locales/or/messages.json index bb84727af7d..d6781321446 100644 --- a/apps/desktop/src/locales/or/messages.json +++ b/apps/desktop/src/locales/or/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/pl/messages.json b/apps/desktop/src/locales/pl/messages.json index c6888c1fb9d..ead83d7fa7c 100644 --- a/apps/desktop/src/locales/pl/messages.json +++ b/apps/desktop/src/locales/pl/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Hasła nie pasują do siebie." - }, - "newAccountCreated": { - "message": "Konto zostało utworzone! Teraz możesz się zalogować." - }, "youSuccessfullyLoggedIn": { "message": "Zalogowałeś się pomyślnie" }, "youMayCloseThisWindow": { "message": "Możesz zamknąć to okno" }, + "masterPassDoesntMatch": { + "message": "Hasła nie pasują do siebie." + }, + "newAccountCreated": { + "message": "Konto zostało utworzone! Teraz możesz się zalogować." + }, "masterPassSent": { "message": "Wysłaliśmy Tobie wiadomość e-mail z podpowiedzią do hasła głównego." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplikacja uwierzytelniająca" }, - "authenticatorAppDesc": { - "message": "Użyj aplikacji mobilnej (np. Authy lub Google Authenticator) do generowania czasowych kodów weryfikacyjnych.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Wprowadź kod wygenerowany przez aplikację uwierzytelniającą, jak Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Klucz bezpieczeństwa YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Klucz bezpieczeństwa Yubico OTP" }, "yubiKeyDesc": { "message": "Użyj YubiKey jako metody dostępu do konta. Działa z YubiKey 4, 4 Nano, 4C i urządzeniami NEO." }, - "duoDesc": { - "message": "Weryfikacja z użyciem Duo Security poprzez aplikację Duo Mobile, SMS, połączenie telefoniczne lub klucz bezpieczeństwa U2F.", + "duoDescV2": { + "message": "Wprowadź kod wygenerowany przez Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Adres e-mail" }, - "emailDesc": { - "message": "Kody weryfikacyjne zostaną wysłane do Ciebie wiadomością e-mail." + "emailDescV2": { + "message": "Wpisz kod wysłany na Twój adres e-mail." }, "loginUnavailable": { "message": "Logowanie jest niedostępne" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Potwierdź hasło pliku" }, + "exportSuccess": { + "message": "Dane sejfu zostały wyeksportowane" + }, "multifactorAuthenticationCancelled": { "message": "Uwierzytelnianie wieloskładnikowe zostało anulowane" }, diff --git a/apps/desktop/src/locales/pt_BR/messages.json b/apps/desktop/src/locales/pt_BR/messages.json index d5198c49aa4..de5ed3fc600 100644 --- a/apps/desktop/src/locales/pt_BR/messages.json +++ b/apps/desktop/src/locales/pt_BR/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "A confirmação da senha mestra não corresponde." - }, - "newAccountCreated": { - "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." - }, "youSuccessfullyLoggedIn": { "message": "Você logou na sua conta com sucesso" }, "youMayCloseThisWindow": { "message": "Você pode fechar esta janela" }, + "masterPassDoesntMatch": { + "message": "A confirmação da senha mestra não corresponde." + }, + "newAccountCreated": { + "message": "A sua nova conta foi criada! Agora você pode iniciar a sessão." + }, "masterPassSent": { "message": "Enviamos um e-mail com a dica da sua senha mestra." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplicativo de Autenticação" }, - "authenticatorAppDesc": { - "message": "Utilize um aplicativo de autenticação (tal como Authy ou Google Authenticator) para gerar códigos de verificação baseados no tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chave de Segurança YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para acessar a sua conta. Funciona com YubiKey 4, 4 Nano, 4C, e dispositivos NEO." }, - "duoDesc": { - "message": "Verifique com o Duo Security utilizando o aplicativo Duo Mobile, SMS, chamada telefônica, ou chave de segurança U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Os códigos de verificação vão ser enviados por e-mail para você." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Sessão Indisponível" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirmar senha do arquivo" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Autenticação de múltiplos fatores cancelada" }, diff --git a/apps/desktop/src/locales/pt_PT/messages.json b/apps/desktop/src/locales/pt_PT/messages.json index 4301627958c..a1b9d5d6238 100644 --- a/apps/desktop/src/locales/pt_PT/messages.json +++ b/apps/desktop/src/locales/pt_PT/messages.json @@ -151,7 +151,7 @@ "message": "Código de segurança" }, "identityName": { - "message": "Nome de identidade" + "message": "Nome da identidade" }, "company": { "message": "Empresa" @@ -266,7 +266,7 @@ "message": "Nome próprio" }, "middleName": { - "message": "Segundo nome" + "message": "Nome do meio" }, "lastName": { "message": "Apelido" @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "A confirmação da palavra-passe mestra não corresponde." - }, - "newAccountCreated": { - "message": "A sua nova conta foi criada! Pode agora iniciar sessão." - }, "youSuccessfullyLoggedIn": { "message": "Iniciou sessão com sucesso" }, "youMayCloseThisWindow": { "message": "Pode fechar esta janela" }, + "masterPassDoesntMatch": { + "message": "A confirmação da palavra-passe mestra não corresponde." + }, + "newAccountCreated": { + "message": "A sua nova conta foi criada! Pode agora iniciar sessão." + }, "masterPassSent": { "message": "Enviámos-lhe um e-mail com a dica da sua palavra-passe mestra." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplicação de autenticação" }, - "authenticatorAppDesc": { - "message": "Utilize uma aplicação de autenticação (como o Authy ou o Google Authenticator) para gerar códigos de verificação baseados no tempo.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Introduza um código gerado por uma aplicação de autenticação como o Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Chave de segurança YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Chave de segurança Yubico OTP" }, "yubiKeyDesc": { "message": "Utilize uma YubiKey para aceder à sua conta. Funciona com os dispositivos YubiKey 4, 4 Nano, 4C e NEO." }, - "duoDesc": { - "message": "Verifique com a Duo Security utilizando a aplicação Duo Mobile, SMS, chamada telefónica ou chave de segurança U2F.", + "duoDescV2": { + "message": "Introduza um código gerado pelo Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Os códigos de verificação ser-lhe-ão enviados por e-mail." + "emailDescV2": { + "message": "Introduza um código enviado para o seu e-mail." }, "loginUnavailable": { "message": "Início de sessão indisponível" @@ -1460,7 +1460,7 @@ "message": "Desbloquear com PIN" }, "setYourPinCode": { - "message": "Defina o seu código PIN para desbloquear o Bitwarden. As suas definições de PIN serão redefinidas se alguma vez terminar sessão completamente da aplicação." + "message": "Defina o seu código PIN para desbloquear o Bitwarden. As suas definições de PIN serão redefinidas se alguma vez terminar sessão por completo da aplicação." }, "pinRequired": { "message": "É necessário o código PIN." @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirmar a palavra-passe do ficheiro" }, + "exportSuccess": { + "message": "Dados do cofre exportados" + }, "multifactorAuthenticationCancelled": { "message": "Autenticação multifator cancelada" }, diff --git a/apps/desktop/src/locales/ro/messages.json b/apps/desktop/src/locales/ro/messages.json index 6d56b5f7d31..874240889ed 100644 --- a/apps/desktop/src/locales/ro/messages.json +++ b/apps/desktop/src/locales/ro/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Parola principală și confirmarea ei nu coincid!" - }, - "newAccountCreated": { - "message": "Noul dvs. cont a fost creat! Acum vă puteți autentifica." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Parola principală și confirmarea ei nu coincid!" + }, + "newAccountCreated": { + "message": "Noul dvs. cont a fost creat! Acum vă puteți autentifica." + }, "masterPassSent": { "message": "V-am trimis un e-mail cu indiciul parolei principale." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Aplicația Autentificator" }, - "authenticatorAppDesc": { - "message": "Utilizați o aplicație de autentificare (cum ar fi Authy sau Google Authenticator) pentru a genera codurile de verificare bazate pe timp.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Cheie de securitate YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Utilizați un YubiKey pentru a vă accesa contul. Funcționează cu dispozitivele YubiKey 4, 4 Nano, 4C și NEO." }, - "duoDesc": { - "message": "Verificați cu Duo Security utilizând aplicația Duo Mobile, SMS, apel telefonic sau cheia de securitate U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-mail" }, - "emailDesc": { - "message": "Codurile de verificare vor fi trimise prin e-mail." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Autentificare indisponibilă" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/ru/messages.json b/apps/desktop/src/locales/ru/messages.json index 780facaab26..92330eeb3aa 100644 --- a/apps/desktop/src/locales/ru/messages.json +++ b/apps/desktop/src/locales/ru/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Мастер-пароли не совпадают." - }, - "newAccountCreated": { - "message": "Аккаунт создан! Теперь вы можете войти в систему." - }, "youSuccessfullyLoggedIn": { "message": "Вы успешно авторизовались" }, "youMayCloseThisWindow": { "message": "Можете закрыть это окно" }, + "masterPassDoesntMatch": { + "message": "Мастер-пароли не совпадают." + }, + "newAccountCreated": { + "message": "Аккаунт создан! Теперь вы можете войти в систему." + }, "masterPassSent": { "message": "Мы отправили вам письмо с подсказкой к мастер-паролю." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Приложение-аутентификатор" }, - "authenticatorAppDesc": { - "message": "Используйте приложение-аутентификатор (например Authy или Google Authenticator) для создания кодов проверки на основе времени.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Введите код, сгенерированный приложением-аутентификатором, например Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ безопасности YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Ключ безопасности Yubico OTP" }, "yubiKeyDesc": { "message": "Используйте YubiKey для доступа к аккаунту. Работает с устройствами YubiKey серий 4, 5 и NEO." }, - "duoDesc": { - "message": "Подтвердите с помощью Duo Security, используя приложение Duo Mobile, SMS, телефонный звонок или ключ безопасности U2F.", + "duoDescV2": { + "message": "Введите код, сгенерированный Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Коды подтверждения будут отправлены вам по электронной почте." + "emailDescV2": { + "message": "Введите код, отправленный на ваш email." }, "loginUnavailable": { "message": "Вход недоступен" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Подтвердите пароль к файлу" }, + "exportSuccess": { + "message": "Данные хранилища экспортированы" + }, "multifactorAuthenticationCancelled": { "message": "Многофакторная аутентификация отменена" }, diff --git a/apps/desktop/src/locales/si/messages.json b/apps/desktop/src/locales/si/messages.json index 9b54752a16e..906181c7bdb 100644 --- a/apps/desktop/src/locales/si/messages.json +++ b/apps/desktop/src/locales/si/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "වි-තැපෑල" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/sk/messages.json b/apps/desktop/src/locales/sk/messages.json index d45ebff7f5e..42ebe6965b3 100644 --- a/apps/desktop/src/locales/sk/messages.json +++ b/apps/desktop/src/locales/sk/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Potvrdenie hlavného hesla sa nezhoduje." - }, - "newAccountCreated": { - "message": "Váš nový účet bol vytvorený! Teraz sa môžete prihlásiť." - }, "youSuccessfullyLoggedIn": { "message": "Úspešne ste sa prihlásili" }, "youMayCloseThisWindow": { "message": "Toto okno môžete zavrieť" }, + "masterPassDoesntMatch": { + "message": "Potvrdenie hlavného hesla sa nezhoduje." + }, + "newAccountCreated": { + "message": "Váš nový účet bol vytvorený! Teraz sa môžete prihlásiť." + }, "masterPassSent": { "message": "Emailom sme vám poslali nápoveď k hlavnému heslu." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Overovacia aplikácia" }, - "authenticatorAppDesc": { - "message": "Použite overovaciu aplikáciu (napríklad Authy alebo Google Authenticator) na generovanie časovo obmedzených overovacích kódov.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Zadajte kód vygenerovaný overovacou aplikáciou akou je napríklad Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP bezpečnostný kľúč" + "yubiKeyTitleV2": { + "message": "Bezpečnostný kľúč Yubico OTP" }, "yubiKeyDesc": { "message": "Použiť YubiKey pre prístup k vášmu účtu. Pracuje s YubiKey 4, 4 Nano, 4C a s NEO zariadeniami." }, - "duoDesc": { - "message": "Overiť sa prostredníctvom Duo Security použitím Duo Mobile aplikácie, SMS, telefonátu alebo U2F bezpečnostným kľúčom.", + "duoDescV2": { + "message": "Zadajte kód vygenerovaný aplikáciou Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Overovacie kódy vám budú zaslané emailom." + "emailDescV2": { + "message": "Zadajte kód zaslaný na váš e-mail." }, "loginUnavailable": { "message": "Prihlásenie nedostupné" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Potvrdiť heslo súboru" }, + "exportSuccess": { + "message": "Údaje z trezora boli exportované" + }, "multifactorAuthenticationCancelled": { "message": "Viacfaktorové overenie zrušené" }, diff --git a/apps/desktop/src/locales/sl/messages.json b/apps/desktop/src/locales/sl/messages.json index 7d3396e18d8..e275b396620 100644 --- a/apps/desktop/src/locales/sl/messages.json +++ b/apps/desktop/src/locales/sl/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Avtentikacijska aplikacija" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-pošta" }, - "emailDesc": { - "message": "Potrditvene kode vam bodo posredovane po e-pošti." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Prijava ni na voljo" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/sr/messages.json b/apps/desktop/src/locales/sr/messages.json index ff8d0c32822..e254b694c34 100644 --- a/apps/desktop/src/locales/sr/messages.json +++ b/apps/desktop/src/locales/sr/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Потврђена Главна Лозинка се не подудара." - }, - "newAccountCreated": { - "message": "Ваш налог је креиран! Сада се можете пријавити." - }, "youSuccessfullyLoggedIn": { "message": "Успешно сте се пријавили" }, "youMayCloseThisWindow": { "message": "Можете затворити овај прозор" }, + "masterPassDoesntMatch": { + "message": "Потврђена Главна Лозинка се не подудара." + }, + "newAccountCreated": { + "message": "Ваш налог је креиран! Сада се можете пријавити." + }, "masterPassSent": { "message": "Послали смо Вам поруку са саветом главне лозинке." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Апликација Аутентификатор" }, - "authenticatorAppDesc": { - "message": "Користите апликацију за аутентификацију (као што је Authy или Google Authenticator) за генерисање верификационих кодова.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Унесите кôд који генерише апликација за аутентификацију као што је Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP сигурносни кључ" + "yubiKeyTitleV2": { + "message": "Yubico OTP сигурносни кључ" }, "yubiKeyDesc": { "message": "Користите YubiKey за приступ налогу. Ради са YubiKey 4, 4 Nano, 4C, и NEO уређајима." }, - "duoDesc": { - "message": "Провери са Duo Security користећи Duo Mobile апликацију, СМС, телефонски позив, или U2F кључ.", + "duoDescV2": { + "message": "Унесите кôд који је генерисао Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Имејл" }, - "emailDesc": { - "message": "Верификациони кодови ће вам бити послати имејлом." + "emailDescV2": { + "message": "Унесите кôд послат на ваш имејл." }, "loginUnavailable": { "message": "Пријава недоступна" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Потврдити лозинку датотеке" }, + "exportSuccess": { + "message": "Подаци из сефа су извезени" + }, "multifactorAuthenticationCancelled": { "message": "Вишефакторска аутентификација је отказана" }, diff --git a/apps/desktop/src/locales/sv/messages.json b/apps/desktop/src/locales/sv/messages.json index 252f077b019..3e73b2598b7 100644 --- a/apps/desktop/src/locales/sv/messages.json +++ b/apps/desktop/src/locales/sv/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Huvudlösenorden stämmer inte överens." - }, - "newAccountCreated": { - "message": "Ditt nya konto har skapats! Du kan nu logga in." - }, "youSuccessfullyLoggedIn": { "message": "Du är nu inloggad" }, "youMayCloseThisWindow": { "message": "Du kan stänga detta fönster" }, + "masterPassDoesntMatch": { + "message": "Huvudlösenorden stämmer inte överens." + }, + "newAccountCreated": { + "message": "Ditt nya konto har skapats! Du kan nu logga in." + }, "masterPassSent": { "message": "Vi har skickat ett e-postmeddelande till dig med din huvudlösenordsledtråd." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Autentiseringsapp" }, - "authenticatorAppDesc": { - "message": "Använd en autentiseringsapp (t.ex. Authy eller Google Authenticator) för att skapa tidsbaserade verifieringskoder.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP-säkerhetsnyckel" + "yubiKeyTitleV2": { + "message": "Yubico OTP-säkerhetsnyckel" }, "yubiKeyDesc": { "message": "Använd en YubiKey för att komma åt ditt konto. Fungerar med YubiKey 4, 4 Nano, 4C och NEO-enheter." }, - "duoDesc": { - "message": "Verifiera med Duo Security genom att använda Duo Mobile-appen, SMS, telefonsamtal eller en U2F-säkerhetsnyckel.", + "duoDescV2": { + "message": "Ange en kod som genererats av Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-post" }, - "emailDesc": { - "message": "Verifieringskoder kommer att skickas till dig via e-post." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Inloggning ej tillgänglig" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Bekräfta fillösenord" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Flerfaktorsautentisering avbruten" }, diff --git a/apps/desktop/src/locales/te/messages.json b/apps/desktop/src/locales/te/messages.json index 767619b10f4..0887d769823 100644 --- a/apps/desktop/src/locales/te/messages.json +++ b/apps/desktop/src/locales/te/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Master password confirmation does not match." - }, - "newAccountCreated": { - "message": "Your new account has been created! You may now log in." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Master password confirmation does not match." + }, + "newAccountCreated": { + "message": "Your new account has been created! You may now log in." + }, "masterPassSent": { "message": "We've sent you an email with your master password hint." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Authenticator app" }, - "authenticatorAppDesc": { - "message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP security key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices." }, - "duoDesc": { - "message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Verification codes will be emailed to you." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Login unavailable" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/th/messages.json b/apps/desktop/src/locales/th/messages.json index 43a8688cf25..8c0a06ec424 100644 --- a/apps/desktop/src/locales/th/messages.json +++ b/apps/desktop/src/locales/th/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "การยืนยันรหัสผ่านหลักไม่ตรงกัน" - }, - "newAccountCreated": { - "message": "บัญชีใหม่ของคุณถูกสร้างขึ้นแล้ว! ตอนนี้คุณสามารถเข้าสู่ระบบ" - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "การยืนยันรหัสผ่านหลักไม่ตรงกัน" + }, + "newAccountCreated": { + "message": "บัญชีใหม่ของคุณถูกสร้างขึ้นแล้ว! ตอนนี้คุณสามารถเข้าสู่ระบบ" + }, "masterPassSent": { "message": "เราได้ส่งอีเมลพร้อมคำใบ้รหัสผ่านหลักของคุณ" }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "แอป Authenticator" }, - "authenticatorAppDesc": { - "message": "ใช้แอปตรวจสอบความถูกต้อง (เช่น Authy หรือ Google Authenticator) เพื่อสร้างรหัสยืนยันตามเวลา", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP Security Key" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "ใช้ YubiKey เพื่อเข้าถึงบัญชีของคุณ ทำงานร่วมกับอุปกรณ์ YubiKey 4, 4 Nano, 4C และ NEO" }, - "duoDesc": { - "message": "ยืนยันด้วย Duo Security โดยใช้แอป Duo Mobile, SMS, โทรศัพท์ หรือคีย์ความปลอดภัย U2F", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "อีเมล" }, - "emailDesc": { - "message": "รหัสยืนยันจะถูกส่งถึงคุณทางอีเมลแล้ว" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "เข้าสู่ระบบไม่พร้อมใช้งาน" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/tr/messages.json b/apps/desktop/src/locales/tr/messages.json index 85f1db15d3c..b44135e8d43 100644 --- a/apps/desktop/src/locales/tr/messages.json +++ b/apps/desktop/src/locales/tr/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Parola ve parola onayı eşleşmiyor." - }, - "newAccountCreated": { - "message": "Yeni hesabınız oluşturuldu! Şimdi giriş yapabilirsiniz." - }, "youSuccessfullyLoggedIn": { "message": "Başarıyla giriş yaptınız" }, "youMayCloseThisWindow": { "message": "Bu pencereyi kapatabilirsiniz" }, + "masterPassDoesntMatch": { + "message": "Parola ve parola onayı eşleşmiyor." + }, + "newAccountCreated": { + "message": "Yeni hesabınız oluşturuldu! Şimdi giriş yapabilirsiniz." + }, "masterPassSent": { "message": "Size ana parolanızın ipucunu içeren bir e-posta gönderdik." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Kimlik doğrulama uygulaması" }, - "authenticatorAppDesc": { - "message": "Zamana dayalı doğrulama kodları oluşturmak için bir kimlik doğrulayıcı uygulaması (Authy veya Google Authenticator gibi) kullanın.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP güvenlik anahtarı" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Hesabınıza erişmek için bir YubiKey kullanın. YubiKey 4, 4 Nano, 4C ve NEO cihazlarıyla çalışır." }, - "duoDesc": { - "message": "Duo Security ile doğrulama için Duo Mobile uygulaması, SMS, telefon görüşmesi veya U2F güvenlik anahtarını kullanın.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "E-posta" }, - "emailDesc": { - "message": "Doğrulama kodu size e-postalanacak." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Giriş yapılamıyor" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Dosya parolasını onaylayın" }, + "exportSuccess": { + "message": "Kasa verileri dışa aktarıldı" + }, "multifactorAuthenticationCancelled": { "message": "Çok faktörlü kimlik doğrulama iptal edildi" }, diff --git a/apps/desktop/src/locales/uk/messages.json b/apps/desktop/src/locales/uk/messages.json index 6dd1b4921ca..9027b8d627c 100644 --- a/apps/desktop/src/locales/uk/messages.json +++ b/apps/desktop/src/locales/uk/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Підтвердження головного пароля не збігається." - }, - "newAccountCreated": { - "message": "Ваш обліковий запис створений! Тепер ви можете увійти." - }, "youSuccessfullyLoggedIn": { "message": "Ви успішно увійшли в систему" }, "youMayCloseThisWindow": { "message": "Можете закрити це вікно" }, + "masterPassDoesntMatch": { + "message": "Підтвердження головного пароля не збігається." + }, + "newAccountCreated": { + "message": "Ваш обліковий запис створений! Тепер ви можете увійти." + }, "masterPassSent": { "message": "Ми надіслали вам лист з підказкою для головного пароля." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Програма автентифікації" }, - "authenticatorAppDesc": { - "message": "Використовуйте програму автентифікації (наприклад, Authy або Google Authenticator), щоб генерувати тимчасові коди підтвердження.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Введіть код, згенерований програмою для автентифікації, як-от Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Ключ безпеки YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Ключ безпеки Yubico OTP" }, "yubiKeyDesc": { "message": "Використовуйте YubiKey для доступу до сховища. Працює з YubiKey 4, 4 Nano, 4C та пристроями NEO." }, - "duoDesc": { - "message": "Авторизуйтесь за допомогою Duo Security з використанням мобільного додатку Duo Mobile, SMS, телефонного виклику, або ключа безпеки U2F.", + "duoDescV2": { + "message": "Введіть код, згенерований Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Е-пошта" }, - "emailDesc": { - "message": "Коди підтвердження будуть надсилатися на вашу пошту." + "emailDescV2": { + "message": "Введіть код, надісланий вам електронною поштою." }, "loginUnavailable": { "message": "Вхід недоступний" @@ -736,7 +736,7 @@ "message": "URL-адреса сервера API" }, "webVaultUrl": { - "message": "URL-адреса сервера веб сховища" + "message": "URL-адреса сервера вебсховища" }, "identityUrl": { "message": "URL-адреса сервера ідентифікації" @@ -846,7 +846,7 @@ "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." }, "goToWebVault": { - "message": "Перейти у веб сховище" + "message": "Перейти у вебсховище" }, "getMobileApp": { "message": "Отримати мобільний додаток" @@ -1837,7 +1837,7 @@ "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendPasswordDesc": { - "message": "За бажанням вимагати пароль в користувачів для доступу до цього відправлення.", + "message": "Ви можете встановити пароль для доступу до цього відправлення.", "description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated." }, "sendNotesDesc": { @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Підтвердьте пароль файлу" }, + "exportSuccess": { + "message": "Дані сховища експортовано" + }, "multifactorAuthenticationCancelled": { "message": "Багатофакторну автентифікацію скасовано" }, diff --git a/apps/desktop/src/locales/vi/messages.json b/apps/desktop/src/locales/vi/messages.json index e3fe91447f9..6e155b82a75 100644 --- a/apps/desktop/src/locales/vi/messages.json +++ b/apps/desktop/src/locales/vi/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "Xác nhận mật khẩu chính không khớp." - }, - "newAccountCreated": { - "message": "Tài khoản của bạn đã được tạo. Bạn có thể đăng nhập bây giờ." - }, "youSuccessfullyLoggedIn": { "message": "You successfully logged in" }, "youMayCloseThisWindow": { "message": "You may close this window" }, + "masterPassDoesntMatch": { + "message": "Xác nhận mật khẩu chính không khớp." + }, + "newAccountCreated": { + "message": "Tài khoản của bạn đã được tạo. Bạn có thể đăng nhập bây giờ." + }, "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." }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "Ứng dụng xác thực" }, - "authenticatorAppDesc": { - "message": "Sử dụng một ứng dụng xác thực (chẳng hạn như Authy hoặc Google Authenticator) để tạo các mã xác nhận theo thời gian thực.", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "Khóa bảo mật YubiKey OTP" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "Sử dụng YubiKey để truy cập tài khoản của bạn. Làm việc với thiết bị YubiKey 4, 4 Nano, 4C và NEO." }, - "duoDesc": { - "message": "Xác minh với Duo Security sử dụng ứng dụng Duo Mobile, SMS, cuộc gọi điện thoại, hoặc khoá bảo mật U2F.", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "Email" }, - "emailDesc": { - "message": "Mã xác thực sẽ được gửi qua email cho bạn." + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "Đăng nhập không sẵn có" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "Confirm file password" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "Multifactor authentication cancelled" }, diff --git a/apps/desktop/src/locales/zh_CN/messages.json b/apps/desktop/src/locales/zh_CN/messages.json index b75c2e04c56..0d25428e768 100644 --- a/apps/desktop/src/locales/zh_CN/messages.json +++ b/apps/desktop/src/locales/zh_CN/messages.json @@ -503,7 +503,7 @@ "message": "设置强密码" }, "finishCreatingYourAccountBySettingAPassword": { - "message": "设置密码后就能完成账户创建" + "message": "设置密码以完成账户的创建" }, "logIn": { "message": "登录" @@ -527,7 +527,7 @@ "message": "主密码提示(可选)" }, "masterPassHintText": { - "message": "If you forget your password, the password hint can be sent to your email. $CURRENT$/$MAXIMUM$ character maximum.", + "message": "如果您忘记了密码,可以发送密码提示到您的电子邮箱。$CURRENT$ / 最多 $MAXIMUM$ 个字符。", "placeholders": { "current": { "content": "$1", @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "两次填写的主密码不一致。" - }, - "newAccountCreated": { - "message": "您的新账户已创建!您现在可以登录了。" - }, "youSuccessfullyLoggedIn": { "message": "您已成功登录" }, "youMayCloseThisWindow": { "message": "您可以关闭此窗口" }, + "masterPassDoesntMatch": { + "message": "两次填写的主密码不一致。" + }, + "newAccountCreated": { + "message": "您的新账户已创建!您现在可以登录了。" + }, "masterPassSent": { "message": "我们已经为您发送了包含主密码提示的邮件。" }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "验证器 App" }, - "authenticatorAppDesc": { - "message": "使用验证器 App(例如 Authy 或 Google Authenticator)来生成基于时间的验证码。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "输入验证器 App(例如 Bitwarden 验证器)生成的代码。", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 安全钥匙" + "yubiKeyTitleV2": { + "message": "Yubico OTP 安全钥匙" }, "yubiKeyDesc": { "message": "使用 YubiKey 来访问您的账户。支持 YubiKey 4、4 Nano、4C 以及 NEO 设备。" }, - "duoDesc": { - "message": "使用 Duo Security 的 Duo 移动应用、短信、电话或 U2F 安全钥匙来进行验证。", + "duoDescV2": { + "message": "输入由 Duo Security 生成的代码。", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "电子邮件" }, - "emailDesc": { - "message": "验证码将会发送到您的电子邮箱。" + "emailDescV2": { + "message": "输入发送到您的电子邮箱的代码。" }, "loginUnavailable": { "message": "登录不可用" @@ -763,7 +763,7 @@ "message": "覆盖密码" }, "learnMore": { - "message": "了解更多" + "message": "进一步了解" }, "featureUnavailable": { "message": "功能不可用" @@ -1148,7 +1148,7 @@ "message": "感谢您支持 Bitwarden。" }, "premiumPrice": { - "message": "每年只需 $PRICE$ !", + "message": "只需 $PRICE$ /年!", "placeholders": { "price": { "content": "$1", @@ -2706,7 +2706,7 @@ "message": "子菜单" }, "toggleSideNavigation": { - "message": "Toggle side navigation" + "message": "切换侧边导航" }, "skipToContent": { "message": "跳转到内容" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "确认文件密码" }, + "exportSuccess": { + "message": "密码库数据已导出" + }, "multifactorAuthenticationCancelled": { "message": "多重身份验证已取消" }, diff --git a/apps/desktop/src/locales/zh_TW/messages.json b/apps/desktop/src/locales/zh_TW/messages.json index e981434905d..75ac7dfb3fe 100644 --- a/apps/desktop/src/locales/zh_TW/messages.json +++ b/apps/desktop/src/locales/zh_TW/messages.json @@ -573,18 +573,18 @@ } } }, - "masterPassDoesntMatch": { - "message": "兩次填入的主密碼不一致。" - }, - "newAccountCreated": { - "message": "帳戶已建立!現在可以登入了。" - }, "youSuccessfullyLoggedIn": { "message": "你已成功登入" }, "youMayCloseThisWindow": { "message": "你可以關閉此視窗" }, + "masterPassDoesntMatch": { + "message": "兩次填入的主密碼不一致。" + }, + "newAccountCreated": { + "message": "帳戶已建立!現在可以登入了。" + }, "masterPassSent": { "message": "已寄出包含您主密碼提示的電子郵件。" }, @@ -666,18 +666,18 @@ "authenticatorAppTitle": { "message": "驗證器應用程式" }, - "authenticatorAppDesc": { - "message": "使用驗證器應用程式(如 Authy 或 Google Authenticator)產生基於時間的驗證碼。", - "description": "'Authy' and 'Google Authenticator' are product names and should not be translated." + "authenticatorAppDescV2": { + "message": "Enter a code generated by an authenticator app like Bitwarden Authenticator.", + "description": "'Bitwarden Authenticator' is a product name and should not be translated." }, - "yubiKeyTitle": { - "message": "YubiKey OTP 安全鑰匙" + "yubiKeyTitleV2": { + "message": "Yubico OTP security key" }, "yubiKeyDesc": { "message": "使用 YubiKey 來存取您的帳戶。支援 YubiKey 4、4 Nano、4C、以及 NEO 裝置。" }, - "duoDesc": { - "message": "使用 Duo Security 的 Duo Mobile 程式、SMS 、致電或 U2F 安全鑰匙進行驗證。", + "duoDescV2": { + "message": "Enter a code generated by Duo Security.", "description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated." }, "duoOrganizationDesc": { @@ -693,8 +693,8 @@ "emailTitle": { "message": "電子郵件" }, - "emailDesc": { - "message": "使用電子郵件傳送驗證碼給您。" + "emailDescV2": { + "message": "Enter a code sent to your email." }, "loginUnavailable": { "message": "登入無法使用" @@ -2843,6 +2843,9 @@ "confirmFilePassword": { "message": "確認檔案密碼" }, + "exportSuccess": { + "message": "Vault data exported" + }, "multifactorAuthenticationCancelled": { "message": "多因素驗證已取消" }, diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 80dbf40cb87..f2295f2cdd8 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -291,8 +291,7 @@ export class Main { }); }, (e: any) => { - // eslint-disable-next-line - console.error(e); + this.logService.error("Error while running migrations:", e); }, ); } diff --git a/apps/desktop/src/models/account.ts b/apps/desktop/src/models/account.ts deleted file mode 100644 index b3d31284139..00000000000 --- a/apps/desktop/src/models/account.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - Account as BaseAccount, - AccountSettings as BaseAccountSettings, -} from "@bitwarden/common/platform/models/domain/account"; - -export class AccountSettings extends BaseAccountSettings { - dismissedBiometricRequirePasswordOnStartCallout?: boolean; -} - -export class Account extends BaseAccount { - settings?: AccountSettings = new AccountSettings(); - - constructor(init: Partial) { - super(init); - Object.assign(this.settings, { - ...new AccountSettings(), - ...this.settings, - }); - } -} diff --git a/apps/desktop/src/package-lock.json b/apps/desktop/src/package-lock.json index 1463087bd33..4ba7c6b6336 100644 --- a/apps/desktop/src/package-lock.json +++ b/apps/desktop/src/package-lock.json @@ -1,15 +1,15 @@ { "name": "@bitwarden/desktop", - "version": "2024.6.6", + "version": "2024.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@bitwarden/desktop", - "version": "2024.6.6", + "version": "2024.7.1", "license": "GPL-3.0", "dependencies": { - "@bitwarden/desktop-native": "file:../desktop_native", + "@bitwarden/desktop-napi": "file:../desktop_native", "argon2": "0.40.1" } }, @@ -21,7 +21,7 @@ "@napi-rs/cli": "2.16.2" } }, - "node_modules/@bitwarden/desktop-native": { + "node_modules/@bitwarden/desktop-napi": { "resolved": "../desktop_native", "link": true }, diff --git a/apps/desktop/src/package.json b/apps/desktop/src/package.json index 2030c29189a..1793642dab6 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.6.6", + "version": "2024.7.1", "author": "Bitwarden Inc. (https://bitwarden.com)", "homepage": "https://bitwarden.com", "license": "GPL-3.0", @@ -12,7 +12,7 @@ "url": "git+https://github.com/bitwarden/clients.git" }, "dependencies": { - "@bitwarden/desktop-native": "file:../desktop_native", + "@bitwarden/desktop-napi": "file:../desktop_native/napi", "argon2": "0.40.1" } } diff --git a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts index 169ee871c75..e1a5c3da9a9 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.darwin.main.ts @@ -1,7 +1,7 @@ import { systemPreferences } from "electron"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { passwords } from "@bitwarden/desktop-native"; +import { passwords } from "@bitwarden/desktop-napi"; import { OsBiometricService } from "./biometrics.service.abstraction"; diff --git a/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts index b715e852b3d..75d5bce8f50 100644 --- a/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts +++ b/apps/desktop/src/platform/main/biometric/biometric.windows.main.ts @@ -2,7 +2,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; -import { biometrics, passwords } from "@bitwarden/desktop-native"; +import { biometrics, passwords } from "@bitwarden/desktop-napi"; import { WindowMain } from "../../../main/window.main"; diff --git a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts index ef8510dd676..cb6bb4858c0 100644 --- a/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts +++ b/apps/desktop/src/platform/main/biometric/biometrics.service.spec.ts @@ -13,7 +13,7 @@ import BiometricWindowsMain from "./biometric.windows.main"; import { BiometricsService } from "./biometrics.service"; import { OsBiometricService } from "./biometrics.service.abstraction"; -jest.mock("@bitwarden/desktop-native", () => { +jest.mock("@bitwarden/desktop-napi", () => { return { biometrics: jest.fn(), passwords: jest.fn(), diff --git a/apps/desktop/src/platform/main/clipboard.main.ts b/apps/desktop/src/platform/main/clipboard.main.ts index c60932b33ba..5ca6aa29529 100644 --- a/apps/desktop/src/platform/main/clipboard.main.ts +++ b/apps/desktop/src/platform/main/clipboard.main.ts @@ -1,6 +1,6 @@ import { ipcMain } from "electron"; -import { clipboards } from "@bitwarden/desktop-native"; +import { clipboards } from "@bitwarden/desktop-napi"; import { ClipboardWriteMessage } from "../types/clipboard"; diff --git a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts index b0601428e0d..adc7935e05a 100644 --- a/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts +++ b/apps/desktop/src/platform/main/desktop-credential-storage-listener.ts @@ -2,7 +2,7 @@ import { ipcMain } from "electron"; import { BiometricKey } from "@bitwarden/common/auth/types/biometric-key"; import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; -import { passwords } from "@bitwarden/desktop-native"; +import { passwords } from "@bitwarden/desktop-napi"; import { BiometricMessage, BiometricAction } from "../../types/biometric-message"; diff --git a/apps/desktop/src/platform/services/electron-state.service.ts b/apps/desktop/src/platform/services/electron-state.service.ts deleted file mode 100644 index 33c97f48afe..00000000000 --- a/apps/desktop/src/platform/services/electron-state.service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state"; -import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service"; - -import { Account } from "../../models/account"; - -export class ElectronStateService extends BaseStateService { - async addAccount(account: Account) { - // Apply desktop overides to default account values - account = new Account(account); - await super.addAccount(account); - } -} diff --git a/apps/desktop/src/platform/services/electron-storage.service.ts b/apps/desktop/src/platform/services/electron-storage.service.ts index dc145fc2f7e..3fa9b2220c5 100644 --- a/apps/desktop/src/platform/services/electron-storage.service.ts +++ b/apps/desktop/src/platform/services/electron-storage.service.ts @@ -82,9 +82,14 @@ export class ElectronStorageService implements AbstractStorageService { } save(key: string, obj: unknown): Promise { + if (obj === undefined) { + return this.remove(key); + } + if (obj instanceof Set) { obj = Array.from(obj); } + this.store.set(key, obj); this.updatesSubject.next({ key, updateType: "save" }); return Promise.resolve(); diff --git a/apps/desktop/src/scss/plugins.scss b/apps/desktop/src/scss/plugins.scss new file mode 100644 index 00000000000..b8ac8697b7f --- /dev/null +++ b/apps/desktop/src/scss/plugins.scss @@ -0,0 +1,31 @@ +@import "variables.scss"; + +@each $mfaType in $mfaTypes { + .mfaType#{$mfaType} { + content: url("../images/two-factor/" + $mfaType + ".png"); + max-width: 100px; + } +} + +.mfaType1 { + @include themify($themes) { + content: url("../images/two-factor/1" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} + +.mfaType7 { + @include themify($themes) { + content: url("../images/two-factor/7" + themed("mfaLogoSuffix")); + max-width: 100px; + } +} + +.recovery-code-img { + @include themify($themes) { + content: url("../images/two-factor/rc" + themed("mfaLogoSuffix")); + max-width: 100px; + max-height: 45px; + } +} diff --git a/apps/desktop/src/scss/styles.scss b/apps/desktop/src/scss/styles.scss index 54c1385dcf0..187b4bf23c8 100644 --- a/apps/desktop/src/scss/styles.scss +++ b/apps/desktop/src/scss/styles.scss @@ -15,5 +15,6 @@ @import "header.scss"; @import "left-nav.scss"; @import "loading.scss"; +@import "plugins.scss"; @import "../../../../libs/angular/src/scss/icons.scss"; @import "../../../../libs/components/src/multi-select/scss/bw.theme"; diff --git a/apps/desktop/src/scss/variables.scss b/apps/desktop/src/scss/variables.scss index 338b30b3cbd..23a4644d3da 100644 --- a/apps/desktop/src/scss/variables.scss +++ b/apps/desktop/src/scss/variables.scss @@ -15,6 +15,8 @@ $list-icon-color: #c7c7cd; $border-radius: 3px; $line-height-base: 1.42857143; +$mfaTypes: 0, 2, 3, 4, 6; + $gray: #555; $gray-light: #777; $text-muted: $gray-light; @@ -92,6 +94,7 @@ $themes: ( infoColor: $brand-info, warningColor: $brand-warning, logoSuffix: "dark", + mfaLogoSuffix: ".png", passwordNumberColor: #007fde, passwordSpecialColor: #c40800, passwordCountText: #212529, @@ -150,6 +153,7 @@ $themes: ( infoColor: #a4b0c6, warningColor: #ffeb66, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: #6f9df1, passwordSpecialColor: #ff8d85, passwordCountText: #ffffff, @@ -208,6 +212,7 @@ $themes: ( infoColor: $nord9, warningColor: $nord12, logoSuffix: "white", + mfaLogoSuffix: "-w.png", passwordNumberColor: $nord8, passwordSpecialColor: $nord12, passwordCountText: $nord5, diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 4512e175ceb..519bd91064c 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -6,13 +6,13 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.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"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { DecryptedCommandData } from "../models/native-messaging/decrypted-command-data"; import { CredentialCreatePayload } from "../models/native-messaging/encrypted-message-payloads/credential-create-payload"; diff --git a/apps/desktop/src/services/native-message-handler.service.ts b/apps/desktop/src/services/native-message-handler.service.ts index c50593768d0..065726559b1 100644 --- a/apps/desktop/src/services/native-message-handler.service.ts +++ b/apps/desktop/src/services/native-message-handler.service.ts @@ -22,7 +22,7 @@ import { UnencryptedMessageResponse } from "../models/native-messaging/unencrypt import { EncryptedMessageHandlerService } from "./encrypted-message-handler.service"; -const EncryptionAlgorithm = "sha1"; +const HashAlgorithmForAsymmetricEncryption = "sha1"; // This service handles messages using the protocol created for the DuckDuckGo integration. @Injectable() @@ -117,7 +117,7 @@ export class NativeMessageHandlerService { const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt( secret, remotePublicKey, - EncryptionAlgorithm, + HashAlgorithmForAsymmetricEncryption, ); this.sendResponse({ diff --git a/apps/desktop/src/services/native-messaging.service.ts b/apps/desktop/src/services/native-messaging.service.ts index 8dc0a57c99c..7f6d39b2e8d 100644 --- a/apps/desktop/src/services/native-messaging.service.ts +++ b/apps/desktop/src/services/native-messaging.service.ts @@ -27,7 +27,7 @@ import { DesktopSettingsService } from "../platform/services/desktop-settings.se import { NativeMessageHandlerService } from "./native-message-handler.service"; const MessageValidTimeout = 10 * 1000; -const EncryptionAlgorithm = "sha1"; +const HashAlgorithmForAsymmetricEncryption = "sha1"; @Injectable() export class NativeMessagingService { @@ -227,7 +227,7 @@ export class NativeMessagingService { const encryptedSecret = await this.cryptoFunctionService.rsaEncrypt( secret, remotePublicKey, - EncryptionAlgorithm, + HashAlgorithmForAsymmetricEncryption, ); ipc.platform.nativeMessaging.sendMessage({ appId: appId, diff --git a/apps/desktop/webpack.main.js b/apps/desktop/webpack.main.js index 83577fcd624..d52a947e362 100644 --- a/apps/desktop/webpack.main.js +++ b/apps/desktop/webpack.main.js @@ -80,7 +80,7 @@ const main = { ], externals: { "electron-reload": "commonjs2 electron-reload", - "@bitwarden/desktop-native": "commonjs2 @bitwarden/desktop-native", + "@bitwarden/desktop-napi": "commonjs2 @bitwarden/desktop-napi", argon2: "commonjs2 argon2", }, diff --git a/apps/web/package.json b/apps/web/package.json index da1bd9c4da1..11bf27b4e39 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "@bitwarden/web-vault", - "version": "2024.6.3", + "version": "2024.7.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/.eslintrc.json b/apps/web/src/app/admin-console/.eslintrc.json new file mode 100644 index 00000000000..d55df3899e7 --- /dev/null +++ b/apps/web/src/app/admin-console/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../../libs/admin-console/.eslintrc.json" +} diff --git a/apps/web/src/app/admin-console/common/new-base.people.component.ts b/apps/web/src/app/admin-console/common/new-base.people.component.ts index 17f504c74aa..90c25e840c0 100644 --- a/apps/web/src/app/admin-console/common/new-base.people.component.ts +++ b/apps/web/src/app/admin-console/common/new-base.people.component.ts @@ -1,7 +1,7 @@ -import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; +import { Directive } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormControl } from "@angular/forms"; -import { firstValueFrom, lastValueFrom, debounceTime } from "rxjs"; +import { firstValueFrom, lastValueFrom, debounceTime, combineLatest, BehaviorSubject } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,88 +18,47 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { DialogService, TableDataSource } from "@bitwarden/components"; +import { DialogService, ToastService } from "@bitwarden/components"; import { OrganizationUserView } from "../organizations/core/views/organization-user.view"; import { UserConfirmComponent } from "../organizations/manage/user-confirm.component"; -type StatusType = OrganizationUserStatusType | ProviderUserStatusType; +import { PeopleTableDataSource, peopleFilter } from "./people-table-data-source"; -const MaxCheckedCount = 500; +export type StatusType = OrganizationUserStatusType | ProviderUserStatusType; +export type UserViewTypes = ProviderUserUserDetailsResponse | OrganizationUserView; /** * A refactored copy of BasePeopleComponent, using the component library table and other modern features. * This will replace BasePeopleComponent once all subclasses have been changed over to use this class. */ @Directive() -export abstract class NewBasePeopleComponent< - UserView extends ProviderUserUserDetailsResponse | OrganizationUserView, -> { - @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) - confirmModalRef: ViewContainerRef; - - get allCount() { - return this.activeUsers != null ? this.activeUsers.length : 0; - } - - get invitedCount() { - return this.statusMap.has(this.userStatusType.Invited) - ? this.statusMap.get(this.userStatusType.Invited).length - : 0; - } - - get acceptedCount() { - return this.statusMap.has(this.userStatusType.Accepted) - ? this.statusMap.get(this.userStatusType.Accepted).length - : 0; - } - - get confirmedCount() { - return this.statusMap.has(this.userStatusType.Confirmed) - ? this.statusMap.get(this.userStatusType.Confirmed).length - : 0; - } - - get revokedCount() { - return this.statusMap.has(this.userStatusType.Revoked) - ? this.statusMap.get(this.userStatusType.Revoked).length - : 0; - } - +export abstract class NewBasePeopleComponent { /** * Shows a banner alerting the admin that users need to be confirmed. */ get showConfirmUsers(): boolean { return ( - this.activeUsers != null && - this.statusMap != null && - this.activeUsers.length > 1 && - this.confirmedCount > 0 && - this.confirmedCount < 3 && - this.acceptedCount > 0 + this.dataSource.activeUserCount > 1 && + this.dataSource.confirmedUserCount > 0 && + this.dataSource.confirmedUserCount < 3 && + this.dataSource.acceptedUserCount > 0 ); } get showBulkConfirmUsers(): boolean { - return this.acceptedCount > 0; + return this.dataSource.acceptedUserCount > 0; } abstract userType: typeof OrganizationUserType | typeof ProviderUserType; abstract userStatusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; - protected dataSource = new TableDataSource(); + protected abstract dataSource: PeopleTableDataSource; firstLoaded: boolean; - /** - * A hashmap that groups users by their status (invited/accepted/etc). This is used by the toggles to show - * user counts and filter data by user status. - */ - statusMap = new Map(); - /** * The currently selected status filter, or null to show all active users. */ @@ -110,22 +69,12 @@ export abstract class NewBasePeopleComponent< */ actionPromise: Promise; - /** - * All users, loaded from the server, before any filtering has been applied. - */ - protected allUsers: UserView[] = []; - - /** - * Active users only, that is, users that are not in the revoked status. - */ - protected activeUsers: UserView[] = []; - protected searchControl = new FormControl("", { nonNullable: true }); + protected statusToggle = new BehaviorSubject(null); constructor( protected apiService: ApiService, protected i18nService: I18nService, - protected platformUtilsService: PlatformUtilsService, protected cryptoService: CryptoService, protected validationService: ValidationService, protected modalService: ModalService, @@ -133,18 +82,19 @@ export abstract class NewBasePeopleComponent< protected userNamePipe: UserNamePipe, protected dialogService: DialogService, protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, + protected toastService: ToastService, ) { - // Connect the search input to the table dataSource filter input - this.searchControl.valueChanges - .pipe(debounceTime(200), takeUntilDestroyed()) - .subscribe((v) => (this.dataSource.filter = v)); + // Connect the search input and status toggles to the table dataSource filter + combineLatest([this.searchControl.valueChanges.pipe(debounceTime(200)), this.statusToggle]) + .pipe(takeUntilDestroyed()) + .subscribe( + ([searchText, status]) => (this.dataSource.filter = peopleFilter(searchText, status)), + ); } abstract edit(user: UserView): void; abstract getUsers(): Promise | UserView[]>; abstract deleteUser(id: string): Promise; - abstract revokeUser(id: string): Promise; - abstract restoreUser(id: string): Promise; abstract reinviteUser(id: string): Promise; abstract confirmUser(user: UserView, publicKey: Uint8Array): Promise; @@ -152,70 +102,16 @@ export abstract class NewBasePeopleComponent< // Load new users from the server const response = await this.getUsers(); - // Reset and repopulate the statusMap - this.statusMap.clear(); - this.activeUsers = []; - for (const status of Utils.iterateEnum(this.userStatusType)) { - this.statusMap.set(status, []); - } - + // GetUsers can return a ListResponse or an Array if (response instanceof ListResponse) { - this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; + this.dataSource.data = response.data != null && response.data.length > 0 ? response.data : []; } else if (Array.isArray(response)) { - this.allUsers = response; + this.dataSource.data = response; } - this.allUsers.forEach((u) => { - if (!this.statusMap.has(u.status)) { - this.statusMap.set(u.status, [u]); - } else { - this.statusMap.get(u.status).push(u); - } - if (u.status !== this.userStatusType.Revoked) { - this.activeUsers.push(u); - } - }); - - // Filter based on UserStatus - this also populates the table on first load - this.filter(this.status); - this.firstLoaded = true; } - /** - * Filter the data source by user status. - * This overwrites dataSource.data because this filtering needs to apply first, before the search input - */ - filter(status: StatusType | null) { - this.status = status; - if (this.status != null) { - this.dataSource.data = this.statusMap.get(this.status); - } else { - this.dataSource.data = this.activeUsers; - } - // Reset checkbox selection - this.selectAll(false); - } - - checkUser(user: UserView, select?: boolean) { - (user as any).checked = select == null ? !(user as any).checked : select; - } - - selectAll(select: boolean) { - if (select) { - // Reset checkbox selection first so we know nothing else is selected - this.selectAll(false); - } - - const filteredUsers = this.dataSource.filteredData; - - const selectCount = - select && filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length; - for (let i = 0; i < selectCount; i++) { - this.checkUser(filteredUsers[i], select); - } - } - invite() { this.edit(null); } @@ -237,59 +133,12 @@ export abstract class NewBasePeopleComponent< this.actionPromise = this.deleteUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), - ); - this.removeUser(user); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } - - protected async revokeUserConfirmationDialog(user: UserView) { - return this.dialogService.openSimpleDialog({ - title: { key: "revokeAccess", placeholders: [this.userNamePipe.transform(user)] }, - content: this.revokeWarningMessage(), - acceptButtonText: { key: "revokeAccess" }, - type: "warning", - }); - } - - async revoke(user: UserView) { - const confirmed = await this.revokeUserConfirmationDialog(user); - - if (!confirmed) { - return false; - } - - this.actionPromise = this.revokeUser(user.id); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("revokedUserId", this.userNamePipe.transform(user)), - ); - await this.load(); - } catch (e) { - this.validationService.showError(e); - } - this.actionPromise = null; - } - - async restore(user: UserView) { - this.actionPromise = this.restoreUser(user.id); - try { - await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("restoredUserId", this.userNamePipe.transform(user)), - ); - await this.load(); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("removedUserId", this.userNamePipe.transform(user)), + }); + this.dataSource.removeUser(user); } catch (e) { this.validationService.showError(e); } @@ -304,11 +153,11 @@ export abstract class NewBasePeopleComponent< this.actionPromise = this.reinviteUser(user.id); try { await this.actionPromise; - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), - ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenReinvited", this.userNamePipe.transform(user)), + }); } catch (e) { this.validationService.showError(e); } @@ -316,25 +165,18 @@ export abstract class NewBasePeopleComponent< } async confirm(user: UserView) { - function updateUser(self: NewBasePeopleComponent) { - user.status = self.userStatusType.Confirmed; - const mapIndex = self.statusMap.get(self.userStatusType.Accepted).indexOf(user); - if (mapIndex > -1) { - self.statusMap.get(self.userStatusType.Accepted).splice(mapIndex, 1); - self.statusMap.get(self.userStatusType.Confirmed).push(user); - } - } - const confirmUser = async (publicKey: Uint8Array) => { try { this.actionPromise = this.confirmUser(user, publicKey); await this.actionPromise; - updateUser(this); - this.platformUtilsService.showToast( - "success", - null, - this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), - ); + user.status = this.userStatusType.Confirmed; + this.dataSource.replaceUser(user); + + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("hasBeenConfirmed", this.userNamePipe.transform(user)), + }); } catch (e) { this.validationService.showError(e); throw e; @@ -379,37 +221,4 @@ export abstract class NewBasePeopleComponent< this.logService.error(`Handled exception: ${e}`); } } - - protected revokeWarningMessage(): string { - return this.i18nService.t("revokeUserConfirmation"); - } - - protected getCheckedUsers() { - return this.dataSource.data.filter((u) => (u as any).checked); - } - - /** - * Remove a user row from the table and all related data sources - */ - protected removeUser(user: UserView) { - let index = this.dataSource.data.indexOf(user); - if (index > -1) { - // Clone the array so that the setter for dataSource.data is triggered to update the table rendering - const updatedData = [...this.dataSource.data]; - updatedData.splice(index, 1); - this.dataSource.data = updatedData; - } - - index = this.allUsers.indexOf(user); - if (index > -1) { - this.allUsers.splice(index, 1); - } - - if (this.statusMap.has(user.status)) { - index = this.statusMap.get(user.status).indexOf(user); - if (index > -1) { - this.statusMap.get(user.status).splice(index, 1); - } - } - } } diff --git a/apps/web/src/app/admin-console/common/people-table-data-source.ts b/apps/web/src/app/admin-console/common/people-table-data-source.ts new file mode 100644 index 00000000000..db357b4dbcd --- /dev/null +++ b/apps/web/src/app/admin-console/common/people-table-data-source.ts @@ -0,0 +1,133 @@ +import { + OrganizationUserStatusType, + ProviderUserStatusType, +} from "@bitwarden/common/admin-console/enums"; +import { TableDataSource } from "@bitwarden/components"; + +import { StatusType, UserViewTypes } from "./new-base.people.component"; + +const MaxCheckedCount = 500; + +/** + * Returns true if the user matches the status, or where the status is `null`, if the user is active (not revoked). + */ +function statusFilter(user: UserViewTypes, status: StatusType) { + if (status == null) { + return user.status != OrganizationUserStatusType.Revoked; + } + + return user.status === status; +} + +/** + * Returns true if the string matches the user's id, name, or email. + * (The default string search includes all properties, which can return false positives for collection names etc.) + */ +function textFilter(user: UserViewTypes, text: string) { + const normalizedText = text?.toLowerCase(); + return ( + !normalizedText || // null/empty strings should be ignored, i.e. always return true + user.email.toLowerCase().includes(normalizedText) || + user.id.toLowerCase().includes(normalizedText) || + user.name?.toLowerCase().includes(normalizedText) + ); +} + +export function peopleFilter(searchText: string, status: StatusType) { + return (user: UserViewTypes) => statusFilter(user, status) && textFilter(user, searchText); +} + +/** + * An extended TableDataSource class for managing people (organization members and provider users). + * It includes a tally of different statuses, utility methods, and other common functionality. + */ +export abstract class PeopleTableDataSource extends TableDataSource { + protected abstract statusType: typeof OrganizationUserStatusType | typeof ProviderUserStatusType; + + /** + * The number of 'active' users, that is, all users who are not in a revoked status. + */ + activeUserCount: number; + + invitedUserCount: number; + acceptedUserCount: number; + confirmedUserCount: number; + revokedUserCount: number; + + override set data(data: T[]) { + super.data = data; + + this.activeUserCount = + this.data?.filter((u) => u.status !== this.statusType.Revoked).length ?? 0; + + this.invitedUserCount = + this.data?.filter((u) => u.status === this.statusType.Invited).length ?? 0; + this.acceptedUserCount = + this.data?.filter((u) => u.status === this.statusType.Accepted).length ?? 0; + this.confirmedUserCount = + this.data?.filter((u) => u.status === this.statusType.Confirmed).length ?? 0; + this.revokedUserCount = + this.data?.filter((u) => u.status === this.statusType.Revoked).length ?? 0; + } + + override get data() { + // If you override a setter, you must also override the getter + return super.data; + } + + /** + * Check or uncheck a user in the table + * @param select check the user (true), uncheck the user (false), or toggle the current state (null) + */ + checkUser(user: T, select?: boolean) { + (user as any).checked = select == null ? !(user as any).checked : select; + } + + getCheckedUsers() { + return this.data.filter((u) => (u as any).checked); + } + + /** + * Check all filtered users (i.e. those rows that are currently visible) + * @param select check the filtered users (true) or uncheck the filtered users (false) + */ + checkAllFilteredUsers(select: boolean) { + if (select) { + // Reset checkbox selection first so we know nothing else is selected + this.uncheckAllUsers(); + } + + const filteredUsers = this.filteredData; + + const selectCount = + filteredUsers.length > MaxCheckedCount ? MaxCheckedCount : filteredUsers.length; + for (let i = 0; i < selectCount; i++) { + this.checkUser(filteredUsers[i], select); + } + } + + uncheckAllUsers() { + this.data.forEach((u) => ((u as any).checked = false)); + } + + /** + * Remove a user from the data source. Use this to ensure the table is re-rendered after the change. + */ + removeUser(user: T) { + // Note: use immutable functions so that we trigger setters to update the table + this.data = this.data.filter((u) => u != user); + } + + /** + * Replace a user in the data source by matching on user.id. Use this to ensure the table is re-rendered after the change. + */ + replaceUser(user: T) { + const index = this.data.findIndex((u) => u.id === user.id); + if (index > -1) { + // Clone the array so that the setter for dataSource.data is triggered to update the table rendering + const updatedData = this.data.slice(); + updatedData[index] = user; + this.data = updatedData; + } + } +} diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts new file mode 100644 index 00000000000..653651bf69a --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.spec.ts @@ -0,0 +1,115 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { MockProxy, any, mock } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { DialogService } from "@bitwarden/components"; + +import { isPaidOrgGuard } from "./is-paid-org.guard"; + +@Component({ + template: "

This is the home screen!

", +}) +export class HomescreenComponent {} + +@Component({ + template: "

This component can only be accessed by a paid organization!

", +}) +export class PaidOrganizationOnlyComponent {} + +@Component({ + template: "

This is the organization upgrade screen!

", +}) +export class OrganizationUpgradeScreenComponent {} + +const orgFactory = (props: Partial = {}) => + Object.assign( + new Organization(), + { + id: "myOrgId", + enabled: true, + type: OrganizationUserType.Admin, + }, + props, + ); + +describe("Is Paid Org Guard", () => { + let organizationService: MockProxy; + let dialogService: MockProxy; + let routerHarness: RouterTestingHarness; + + beforeEach(async () => { + organizationService = mock(); + dialogService = mock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: OrganizationService, useValue: organizationService }, + { provide: DialogService, useValue: dialogService }, + provideRouter([ + { + path: "", + component: HomescreenComponent, + }, + { + path: "organizations/:organizationId/paidOrganizationsOnly", + component: PaidOrganizationOnlyComponent, + canActivate: [isPaidOrgGuard()], + }, + { + path: "organizations/:organizationId/billing/subscription", + component: OrganizationUpgradeScreenComponent, + }, + ]), + ], + }); + + routerHarness = await RouterTestingHarness.create(); + }); + + it("redirects to `/` if the organization id provided is not found", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(null); + await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the home screen!", + ); + }); + + it("shows a dialog to users of a free organization and does not proceed with navigation", async () => { + // `useTotp` is the current indicator of a free org, it is the baseline + // feature offered above the free organization level. + const org = orgFactory({ type: OrganizationUserType.User, useTotp: false }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); + expect(dialogService.openSimpleDialog).toHaveBeenCalled(); + expect( + routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "", + ).not.toBe("This component can only be accessed by a paid organization!"); + }); + + it("redirects users with billing access to the billing screen to upgrade", async () => { + // `useTotp` is the current indicator of a free org, it is the baseline + // feature offered above the free organization level. + const org = orgFactory({ type: OrganizationUserType.Owner, useTotp: false }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + dialogService.openSimpleDialog.calledWith(any()).mockResolvedValue(true); + await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the organization upgrade screen!", + ); + }); + + it("proceeds with navigation if the organization in question is a paid organization", async () => { + const org = orgFactory({ useTotp: true }); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/paidOrganizationsOnly`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This component can only be accessed by a paid organization!", + ); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts index aaf24e48346..e5ac9529c8a 100644 --- a/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/is-paid-org.guard.ts @@ -1,32 +1,37 @@ -import { Injectable } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { DialogService } from "@bitwarden/components"; -@Injectable({ - providedIn: "root", -}) -export class IsPaidOrgGuard implements CanActivate { - constructor( - private router: Router, - private organizationService: OrganizationService, - private messagingService: MessagingService, - private dialogService: DialogService, - ) {} +/** + * `CanActivateFn` that checks if the organization matching the id in the URL + * parameters is paid or free. If the organization is free instructions are + * provided on how to upgrade a free organization, and the user is redirected + * if they have access to upgrade the organization. If the organization is + * paid routing proceeds." + */ +export function isPaidOrgGuard(): CanActivateFn { + return async (route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => { + const router = inject(Router); + const organizationService = inject(OrganizationService); + const dialogService = inject(DialogService); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const org = await this.organizationService.get(route.params.organizationId); + const org = await organizationService.get(route.params.organizationId); if (org == null) { - return this.router.createUrlTree(["/"]); + return router.createUrlTree(["/"]); } if (org.isFreeOrg) { // Users without billing permission can't access billing if (!org.canEditSubscription) { - await this.dialogService.openSimpleDialog({ + await dialogService.openSimpleDialog({ title: { key: "upgradeOrganizationCloseSecurityGaps" }, content: { key: "upgradeOrganizationCloseSecurityGapsDesc" }, acceptButtonText: { key: "ok" }, @@ -35,7 +40,7 @@ export class IsPaidOrgGuard implements CanActivate { }); return false; } else { - const upgradeConfirmed = await this.dialogService.openSimpleDialog({ + const upgradeConfirmed = await dialogService.openSimpleDialog({ title: { key: "upgradeOrganizationCloseSecurityGaps" }, content: { key: "upgradeOrganizationCloseSecurityGapsDesc" }, acceptButtonText: { key: "upgradeOrganization" }, @@ -43,7 +48,7 @@ export class IsPaidOrgGuard implements CanActivate { icon: "bwi-arrow-circle-up", }); if (upgradeConfirmed) { - await this.router.navigate(["organizations", org.id, "billing", "subscription"], { + await router.navigate(["organizations", org.id, "billing", "subscription"], { queryParams: { upgrade: true }, }); } @@ -51,5 +56,5 @@ export class IsPaidOrgGuard implements CanActivate { } return !org.isFreeOrg; - } + }; } diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts index 570c35d5f8f..72e68febe2f 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.spec.ts @@ -1,3 +1,4 @@ +import { TestBed } from "@angular/core/testing"; import { ActivatedRouteSnapshot, convertToParamMap, @@ -10,10 +11,10 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; -import { OrganizationPermissionsGuard } from "./org-permissions.guard"; +import { organizationPermissionsGuard } from "./org-permissions.guard"; const orgFactory = (props: Partial = {}) => Object.assign( @@ -32,8 +33,6 @@ describe("Organization Permissions Guard", () => { let state: MockProxy; let route: MockProxy; - let organizationPermissionsGuard: OrganizationPermissionsGuard; - beforeEach(() => { router = mock(); organizationService = mock(); @@ -42,24 +41,25 @@ describe("Organization Permissions Guard", () => { params: { organizationId: orgFactory().id, }, - data: { - organizationPermissions: null, - }, }); - organizationPermissionsGuard = new OrganizationPermissionsGuard( - router, - organizationService, - mock(), - mock(), - mock(), - ); + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: router }, + { provide: OrganizationService, useValue: organizationService }, + { provide: ToastService, useValue: mock() }, + { provide: I18nService, useValue: mock() }, + { provide: SyncService, useValue: mock() }, + ], + }); }); it("blocks navigation if organization does not exist", async () => { organizationService.get.mockReturnValue(null); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard()(route, state), + ); expect(actual).not.toBe(true); }); @@ -68,22 +68,22 @@ describe("Organization Permissions Guard", () => { const org = orgFactory(); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext(async () => + organizationPermissionsGuard()(route, state), + ); expect(actual).toBe(true); }); it("permits navigation if the user has permissions", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((org) => true); - route.data = { - organizationPermissions: permissionsCallback, - }; - + permissionsCallback.mockImplementation((_org) => true); const org = orgFactory(); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard(permissionsCallback)(route, state), + ); expect(permissionsCallback).toHaveBeenCalled(); expect(actual).toBe(true); @@ -92,10 +92,7 @@ describe("Organization Permissions Guard", () => { describe("if the user does not have permissions", () => { it("and there is no Item ID, block navigation", async () => { const permissionsCallback = jest.fn(); - permissionsCallback.mockImplementation((org) => false); - route.data = { - organizationPermissions: permissionsCallback, - }; + permissionsCallback.mockImplementation((_org) => false); state = mock({ root: mock({ @@ -106,16 +103,15 @@ describe("Organization Permissions Guard", () => { const org = orgFactory(); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard(permissionsCallback)(route, state), + ); expect(permissionsCallback).toHaveBeenCalled(); expect(actual).not.toBe(true); }); it("and there is an Item ID, redirect to the item in the individual vault", async () => { - route.data = { - organizationPermissions: (org: Organization) => false, - }; state = mock({ root: mock({ queryParamMap: convertToParamMap({ @@ -126,7 +122,9 @@ describe("Organization Permissions Guard", () => { const org = orgFactory(); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard((_org: Organization) => false)(route, state), + ); expect(router.createUrlTree).toHaveBeenCalledWith(["/vault"], { queryParams: { itemId: "myItemId" }, @@ -143,7 +141,9 @@ describe("Organization Permissions Guard", () => { }); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard()(route, state), + ); expect(actual).not.toBe(true); }); @@ -155,7 +155,9 @@ describe("Organization Permissions Guard", () => { }); organizationService.get.calledWith(org.id).mockResolvedValue(org); - const actual = await organizationPermissionsGuard.canActivate(route, state); + const actual = await TestBed.runInInjectionContext( + async () => await organizationPermissionsGuard()(route, state), + ); expect(actual).toBe(true); }); diff --git a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts index 63b89284ec6..ded9e2b328b 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-permissions.guard.ts @@ -1,5 +1,10 @@ -import { Injectable } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; import { canAccessOrgAdmin, @@ -7,43 +12,58 @@ import { } 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; +import { ToastService } from "@bitwarden/components"; -@Injectable({ - providedIn: "root", -}) -export class OrganizationPermissionsGuard implements CanActivate { - constructor( - private router: Router, - private organizationService: OrganizationService, - private platformUtilsService: PlatformUtilsService, - private i18nService: I18nService, - private syncService: SyncService, - ) {} +/** + * `CanActivateFn` that asserts the logged in user has permission to access + * the page being navigated to. Two high-level checks are performed: + * + * 1. If the user is not a member of the organization in the URL parameters, they + * are redirected to the home screen. + * 2. If the organization in the URL parameters is disabled and the user is not + * an admin, they are redirected to the home screen. + * + * In addition to these high level checks the guard accepts a callback + * function as an argument that will be called to check for more granular + * permissions. Based on the return from callback one of the following + * will happen: + * + * 1. If the logged in user does not have the required permissions they are + * redirected to `/organizations/{id}` or `/` based on admin console access + * permissions. + * 2. If the logged in user does have the required permissions navigation + * proceeds as expected. + */ +export function organizationPermissionsGuard( + permissionsCallback?: (organization: Organization) => boolean, +): CanActivateFn { + return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const router = inject(Router); + const organizationService = inject(OrganizationService); + const toastService = inject(ToastService); + const i18nService = inject(I18nService); + const syncService = inject(SyncService); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - // TODO: We need to fix this issue once and for all. - if ((await this.syncService.getLastSync()) == null) { - await this.syncService.fullSync(false); + // TODO: We need to fix issue once and for all. + if ((await syncService.getLastSync()) == null) { + await syncService.fullSync(false); } - const org = await this.organizationService.get(route.params.organizationId); + const org = await organizationService.get(route.params.organizationId); if (org == null) { - return this.router.createUrlTree(["/"]); + return router.createUrlTree(["/"]); } if (!org.isOwner && !org.enabled) { - this.platformUtilsService.showToast( - "error", - null, - this.i18nService.t("organizationIsDisabled"), - ); - return this.router.createUrlTree(["/"]); + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("organizationIsDisabled"), + }); + return router.createUrlTree(["/"]); } - const permissionsCallback: (organization: Organization) => boolean = - route.data?.organizationPermissions; const hasPermissions = permissionsCallback == null || permissionsCallback(org); if (!hasPermissions) { @@ -52,19 +72,23 @@ export class OrganizationPermissionsGuard implements CanActivate { const cipherId = state.root.queryParamMap.get("itemId") || state.root.queryParamMap.get("cipherId"); if (cipherId) { - return this.router.createUrlTree(["/vault"], { + return router.createUrlTree(["/vault"], { queryParams: { itemId: cipherId, }, }); } - this.platformUtilsService.showToast("error", null, this.i18nService.t("accessDenied")); + toastService.showToast({ + variant: "error", + title: null, + message: i18nService.t("accessDenied"), + }); return canAccessOrgAdmin(org) - ? this.router.createUrlTree(["/organizations", org.id]) - : this.router.createUrlTree(["/"]); + ? router.createUrlTree(["/organizations", org.id]) + : router.createUrlTree(["/"]); } return true; - } + }; } diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts new file mode 100644 index 00000000000..576a9dde194 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.spec.ts @@ -0,0 +1,123 @@ +import { Component } from "@angular/core"; +import { TestBed } from "@angular/core/testing"; +import { provideRouter } from "@angular/router"; +import { RouterTestingHarness } from "@angular/router/testing"; +import { MockProxy, mock } from "jest-mock-extended"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; + +import { organizationRedirectGuard } from "./org-redirect.guard"; + +@Component({ + template: "

This is the home screen!

", +}) +export class HomescreenComponent {} + +@Component({ + template: "

This is the admin console!

", +}) +export class AdminConsoleComponent {} + +@Component({ + template: "

This is a subroute of the admin console!

", +}) +export class AdminConsoleSubrouteComponent {} + +const orgFactory = (props: Partial = {}) => + Object.assign( + new Organization(), + { + id: "myOrgId", + enabled: true, + type: OrganizationUserType.Admin, + }, + props, + ); + +describe("Organization Redirect Guard", () => { + let organizationService: MockProxy; + let routerHarness: RouterTestingHarness; + + beforeEach(async () => { + organizationService = mock(); + + TestBed.configureTestingModule({ + providers: [ + { provide: OrganizationService, useValue: organizationService }, + provideRouter([ + { + path: "", + component: HomescreenComponent, + }, + { + path: "organizations/:organizationId", + component: AdminConsoleComponent, + }, + { + path: "organizations/:organizationId/stringCallback/success", + component: AdminConsoleSubrouteComponent, + }, + { + path: "organizations/:organizationId/arrayCallback/exponential/success", + component: AdminConsoleSubrouteComponent, + }, + { + path: "organizations/:organizationId/noCallback", + component: AdminConsoleComponent, + canActivate: [organizationRedirectGuard()], + }, + { + path: "organizations/:organizationId/stringCallback", + component: AdminConsoleComponent, + canActivate: [organizationRedirectGuard(() => "success")], + }, + { + path: "organizations/:organizationId/arrayCallback", + component: AdminConsoleComponent, + canActivate: [organizationRedirectGuard(() => ["exponential", "success"])], + }, + ]), + ], + }); + + routerHarness = await RouterTestingHarness.create(); + }); + + it("redirects to `/` if the organization id provided is not found", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(null); + await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the home screen!", + ); + }); + + it("redirects to `/organizations/{id}` if no custom redirect is supplied but the user can access the admin onsole", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/noCallback`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is the admin console!", + ); + }); + + it("redirects properly when the redirect callback returns a single string", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/stringCallback`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is a subroute of the admin console!", + ); + }); + + it("redirects properly when the redirect callback returns an array of strings", async () => { + const org = orgFactory(); + organizationService.get.calledWith(org.id).mockResolvedValue(org); + await routerHarness.navigateByUrl(`organizations/${org.id}/arrayCallback`); + expect(routerHarness.routeNativeElement?.querySelector("h1")?.textContent?.trim() ?? "").toBe( + "This is a subroute of the admin console!", + ); + }); +}); diff --git a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts index bbfb51ed949..1ab73195e4d 100644 --- a/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts +++ b/apps/web/src/app/admin-console/organizations/guards/org-redirect.guard.ts @@ -1,35 +1,45 @@ -import { Injectable } from "@angular/core"; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; +import { inject } from "@angular/core"; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from "@angular/router"; import { canAccessOrgAdmin, OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -@Injectable({ - providedIn: "root", -}) -export class OrganizationRedirectGuard implements CanActivate { - constructor( - private router: Router, - private organizationService: OrganizationService, - ) {} +/** + * + * `CanActivateFn` that returns a URL Tree redirecting to a caller provided + * sub route of `/organizations/{id}/`. If no sub route is provided the URL + * tree returned will redirect to `/organizations/{id}` if possible, or `/` if + * the user does not have permission to access `organizations/{id}`. + */ +export function organizationRedirectGuard( + customRedirect?: (org: Organization) => string | string[], +): CanActivateFn { + return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => { + const router = inject(Router); + const organizationService = inject(OrganizationService); - async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { - const org = await this.organizationService.get(route.params.organizationId); + const org = await organizationService.get(route.params.organizationId); - const customRedirect = route.data?.autoRedirectCallback; - if (customRedirect) { + if (customRedirect != null) { let redirectPath = customRedirect(org); if (typeof redirectPath === "string") { redirectPath = [redirectPath]; } - return this.router.createUrlTree([state.url, ...redirectPath]); + return router.createUrlTree([state.url, ...redirectPath]); } - if (canAccessOrgAdmin(org)) { - return this.router.createUrlTree(["/organizations", org.id]); + if (org != null && canAccessOrgAdmin(org)) { + return router.createUrlTree(["/organizations", org.id]); } - return this.router.createUrlTree(["/"]); - } + + return router.createUrlTree(["/"]); + }; } diff --git a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html index 563905548df..336285b2a45 100644 --- a/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/admin-console/organizations/layouts/organization-layout.component.html @@ -1,5 +1,5 @@ - - + + @@ -103,12 +103,7 @@ *ngIf="organization.canManageScim" > - - - - - - + {{ "accessingUsingProvider" | i18n: organization.providerName }} - - + 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 4383656bee1..65fefe01a3b 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 @@ -22,12 +22,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { BannerModule, IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components"; +import { BannerModule, IconModule } from "@bitwarden/components"; -import { PaymentMethodWarningsModule } from "../../../billing/shared"; import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component"; -import { ProductSwitcherModule } from "../../../layouts/product-switcher/product-switcher.module"; -import { ToggleWidthComponent } from "../../../layouts/toggle-width.component"; +import { WebLayoutModule } from "../../../layouts/web-layout.module"; import { AdminConsoleLogo } from "../../icons/admin-console-logo"; @Component({ @@ -38,14 +36,10 @@ import { AdminConsoleLogo } from "../../icons/admin-console-logo"; CommonModule, RouterModule, JslibModule, - LayoutComponent, + WebLayoutModule, IconModule, - NavigationModule, OrgSwitcherComponent, BannerModule, - PaymentMethodWarningsModule, - ToggleWidthComponent, - ProductSwitcherModule, ], }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { @@ -64,10 +58,6 @@ export class OrganizationLayoutComponent implements OnInit, OnDestroy { FeatureFlag.EnableConsolidatedBilling, ); - protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$( - FeatureFlag.ShowPaymentMethodWarningBanners, - ); - constructor( private route: ActivatedRoute, private organizationService: OrganizationService, diff --git a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts index 9ff596181e3..7c86ac28498 100644 --- a/apps/web/src/app/admin-console/organizations/manage/groups.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/groups.component.ts @@ -71,6 +71,11 @@ type GroupDetailsRow = { collectionNames?: string[]; }; +/** + * @deprecated To be replaced with NewGroupsComponent which significantly refactors this component. + * The GroupsComponentRefactor flag switches between the old and new components; this component will be removed when + * the feature flag is removed. + */ @Component({ selector: "app-org-groups", templateUrl: "groups.component.html", diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html new file mode 100644 index 00000000000..3e659e5b6a8 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.html @@ -0,0 +1,109 @@ + + + + + + + + {{ "loading" | i18n }} + + +

{{ "noGroupsInList" | i18n }}

+ + + + + + + + + + {{ "name" | i18n }} + {{ "collections" | i18n }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts new file mode 100644 index 00000000000..e5e99333e64 --- /dev/null +++ b/apps/web/src/app/admin-console/organizations/manage/new-groups.component.ts @@ -0,0 +1,255 @@ +import { Component } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { FormControl } from "@angular/forms"; +import { ActivatedRoute } from "@angular/router"; +import { + BehaviorSubject, + combineLatest, + concatMap, + from, + lastValueFrom, + map, + switchMap, + tap, +} from "rxjs"; +import { debounceTime, first } from "rxjs/operators"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; +import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; +import { Collection } from "@bitwarden/common/vault/models/domain/collection"; +import { + CollectionDetailsResponse, + CollectionResponse, +} from "@bitwarden/common/vault/models/response/collection.response"; +import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; +import { DialogService, TableDataSource, ToastService } from "@bitwarden/components"; + +import { InternalGroupService as GroupService, GroupView } from "../core"; + +import { + GroupAddEditDialogResultType, + GroupAddEditTabType, + openGroupAddEditDialog, +} from "./group-add-edit.component"; + +type GroupDetailsRow = { + /** + * Details used for displaying group information + */ + details: GroupView; + + /** + * True if the group is selected in the table + */ + checked?: boolean; + + /** + * A list of collection names the group has access to + */ + collectionNames?: string[]; +}; + +/** + * Custom filter predicate that filters the groups table by id and name only. + * This is required because the default implementation searches by all properties, which can unintentionally match + * with members' names (who are assigned to the group) or collection names (which the group has access to). + */ +const groupsFilter = (filter: string) => { + const transformedFilter = filter.trim().toLowerCase(); + return (data: GroupDetailsRow) => { + const group = data.details; + + return ( + group.id.toLowerCase().indexOf(transformedFilter) != -1 || + group.name.toLowerCase().indexOf(transformedFilter) != -1 + ); + }; +}; + +@Component({ + templateUrl: "new-groups.component.html", +}) +export class NewGroupsComponent { + loading = true; + organizationId: string; + + protected dataSource = new TableDataSource(); + protected searchControl = new FormControl(""); + + // Fixed sizes used for cdkVirtualScroll + protected rowHeight = 46; + protected rowHeightClass = `tw-h-[46px]`; + + protected ModalTabType = GroupAddEditTabType; + private refreshGroups$ = new BehaviorSubject(null); + + constructor( + private apiService: ApiService, + private groupService: GroupService, + private route: ActivatedRoute, + private i18nService: I18nService, + private dialogService: DialogService, + private logService: LogService, + private collectionService: CollectionService, + private toastService: ToastService, + ) { + this.route.params + .pipe( + tap((params) => (this.organizationId = params.organizationId)), + switchMap(() => + combineLatest([ + // collectionMap + from(this.apiService.getCollections(this.organizationId)).pipe( + concatMap((response) => this.toCollectionMap(response)), + ), + // groups + this.refreshGroups$.pipe( + switchMap(() => this.groupService.getAll(this.organizationId)), + ), + ]), + ), + map(([collectionMap, groups]) => { + return groups.map((g) => ({ + id: g.id, + name: g.name, + details: g, + checked: false, + collectionNames: g.collections + .map((c) => collectionMap[c.id]?.name) + .sort(this.i18nService.collator?.compare), + })); + }), + takeUntilDestroyed(), + ) + .subscribe((groups) => { + this.dataSource.data = groups; + this.loading = false; + }); + + // Connect the search input to the table dataSource filter input + this.searchControl.valueChanges + .pipe(debounceTime(200), takeUntilDestroyed()) + .subscribe((v) => (this.dataSource.filter = groupsFilter(v))); + + this.route.queryParams.pipe(first(), takeUntilDestroyed()).subscribe((qParams) => { + this.searchControl.setValue(qParams.search); + }); + } + + async edit( + group: GroupDetailsRow, + startingTabIndex: GroupAddEditTabType = GroupAddEditTabType.Info, + ) { + const dialogRef = openGroupAddEditDialog(this.dialogService, { + data: { + initialTab: startingTabIndex, + organizationId: this.organizationId, + groupId: group != null ? group.details.id : null, + }, + }); + + const result = await lastValueFrom(dialogRef.closed); + + if (result == GroupAddEditDialogResultType.Saved) { + this.refreshGroups$.next(); + } else if (result == GroupAddEditDialogResultType.Deleted) { + this.removeGroup(group); + } + } + + async add() { + await this.edit(null); + } + + async delete(groupRow: GroupDetailsRow) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: groupRow.details.name, + content: { key: "deleteGroupConfirmation" }, + type: "warning", + }); + if (!confirmed) { + return false; + } + + try { + await this.groupService.delete(this.organizationId, groupRow.details.id); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedGroupId", groupRow.details.name), + }); + this.removeGroup(groupRow); + } catch (e) { + this.logService.error(e); + } + } + + async deleteAllSelected() { + const groupsToDelete = this.dataSource.data.filter((g) => g.checked); + + if (groupsToDelete.length == 0) { + return; + } + + const deleteMessage = groupsToDelete.map((g) => g.details.name).join(", "); + const confirmed = await this.dialogService.openSimpleDialog({ + title: { + key: "deleteMultipleGroupsConfirmation", + placeholders: [groupsToDelete.length.toString()], + }, + content: deleteMessage, + type: "warning", + }); + if (!confirmed) { + return false; + } + + try { + await this.groupService.deleteMany( + this.organizationId, + groupsToDelete.map((g) => g.details.id), + ); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("deletedManyGroups", groupsToDelete.length.toString()), + }); + + groupsToDelete.forEach((g) => this.removeGroup(g)); + } catch (e) { + this.logService.error(e); + } + } + + check(groupRow: GroupDetailsRow) { + groupRow.checked = !groupRow.checked; + } + + toggleAllVisible(event: Event) { + this.dataSource.filteredData.forEach( + (g) => (g.checked = (event.target as HTMLInputElement).checked), + ); + } + + private removeGroup(groupRow: GroupDetailsRow) { + // Assign a new array to dataSource.data to trigger the setters and update the table + this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow); + } + + private async toCollectionMap(response: ListResponse) { + const collections = response.data.map( + (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)), + ); + const decryptedCollections = await this.collectionService.decryptMany(collections); + + // Convert to an object using collection Ids as keys for faster name lookups + const collectionMap: Record = {}; + decryptedCollections.forEach((c) => (collectionMap[c.id] = c)); + + return collectionMap; + } +} diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts index d823240fe67..69819d69812 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/nested-checkbox.component.ts @@ -1,5 +1,5 @@ import { KeyValue } from "@angular/common"; -import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from "@angular/core"; +import { Component, Input, OnInit, OnDestroy } from "@angular/core"; import { FormControl, FormGroup } from "@angular/forms"; import { Subject, takeUntil } from "rxjs"; @@ -14,8 +14,6 @@ export class NestedCheckboxComponent implements OnInit, OnDestroy { @Input() parentId: string; @Input() checkboxes: FormGroup>>; - @Output() onSavedUser = new EventEmitter(); - @Output() onDeletedUser = new EventEmitter(); get parentIndeterminate() { return ( diff --git a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts index 641d1f2e954..cbda3b2bdf3 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/reset-password.component.ts @@ -17,8 +17,8 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service"; @@ -31,7 +31,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { @Input() email: string; @Input() id: string; @Input() organizationId: string; - @Output() onPasswordReset = new EventEmitter(); + @Output() passwordReset = new EventEmitter(); @ViewChild(PasswordStrengthComponent) passwordStrengthComponent: PasswordStrengthComponent; enforcedPolicyOptions: MasterPasswordPolicyOptions; @@ -156,7 +156,7 @@ export class ResetPasswordComponent implements OnInit, OnDestroy { null, this.i18nService.t("resetPasswordSuccess"), ); - this.onPasswordReset.emit(); + this.passwordReset.emit(); } catch (e) { this.logService.error(e); } diff --git a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts index a1d010deef5..5220ea1ef39 100644 --- a/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/members/members-routing.module.ts @@ -3,18 +3,17 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessMembersTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; +import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { PeopleComponent } from "./people.component"; +import { MembersComponent } from "./members.component"; const routes: Routes = [ { path: "", - component: PeopleComponent, - canActivate: [OrganizationPermissionsGuard], + component: MembersComponent, + canActivate: [organizationPermissionsGuard(canAccessMembersTab)], data: { titleId: "members", - organizationPermissions: canAccessMembersTab, }, }, ]; diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.html b/apps/web/src/app/admin-console/organizations/members/members.component.html similarity index 93% rename from apps/web/src/app/admin-console/organizations/members/people.component.html rename to apps/web/src/app/admin-console/organizations/members/members.component.html index 9123b39a375..99afe8099a6 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.html +++ b/apps/web/src/app/admin-console/organizations/members/members.component.html @@ -14,26 +14,35 @@
- {{ "all" | i18n }} {{ allCount }} + {{ "all" | i18n }} + {{ + allCount + }} {{ "invited" | i18n }} - {{ invitedCount }} + {{ + invitedCount + }} {{ "needsConfirmation" | i18n }} - {{ acceptedCount }} + {{ + acceptedUserCount + }} {{ "revoked" | i18n }} - {{ revokedCount }} + {{ + revokedCount + }}
@@ -67,7 +76,7 @@ type="checkbox" bitCheckbox class="tw-mr-1" - (change)="selectAll($any($event.target).checked)" + (change)="dataSource.checkAllFilteredUsers($any($event.target).checked)" id="selectAll" />
- - - - - - diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/members.component.ts similarity index 87% rename from apps/web/src/app/admin-console/organizations/members/people.component.ts rename to apps/web/src/app/admin-console/organizations/members/members.component.ts index 2349d989955..93827539f8f 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/members.component.ts @@ -37,19 +37,19 @@ import { ProductTierType } from "@bitwarden/common/billing/enums"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data"; import { Collection } from "@bitwarden/common/vault/models/domain/collection"; import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; -import { DialogService, SimpleDialogOptions } from "@bitwarden/components"; +import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components"; -import { openEntityEventsDialog } from "../../../admin-console/organizations/manage/entity-events.component"; import { NewBasePeopleComponent } from "../../common/new-base.people.component"; +import { PeopleTableDataSource } from "../../common/people-table-data-source"; import { GroupService } from "../core"; import { OrganizationUserView } from "../core/views/organization-user.view"; +import { openEntityEventsDialog } from "../manage/entity-events.component"; import { BulkConfirmComponent } from "./components/bulk/bulk-confirm.component"; import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component"; @@ -63,27 +63,21 @@ import { } from "./components/member-dialog"; import { ResetPasswordComponent } from "./components/reset-password.component"; +class MembersTableDataSource extends PeopleTableDataSource { + protected statusType = OrganizationUserStatusType; +} + @Component({ - selector: "app-org-people", - templateUrl: "people.component.html", + templateUrl: "members.component.html", }) -export class PeopleComponent extends NewBasePeopleComponent { - @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) - groupsModalRef: ViewContainerRef; - @ViewChild("confirmTemplate", { read: ViewContainerRef, static: true }) - confirmModalRef: ViewContainerRef; +export class MembersComponent extends NewBasePeopleComponent { @ViewChild("resetPasswordTemplate", { read: ViewContainerRef, static: true }) resetPasswordModalRef: ViewContainerRef; - @ViewChild("bulkStatusTemplate", { read: ViewContainerRef, static: true }) - bulkStatusModalRef: ViewContainerRef; - @ViewChild("bulkConfirmTemplate", { read: ViewContainerRef, static: true }) - bulkConfirmModalRef: ViewContainerRef; - @ViewChild("bulkRemoveTemplate", { read: ViewContainerRef, static: true }) - bulkRemoveModalRef: ViewContainerRef; userType = OrganizationUserType; userStatusType = OrganizationUserStatusType; memberTab = MemberDialogTab; + protected dataSource = new MembersTableDataSource(); organization: Organization; status: OrganizationUserStatusType = null; @@ -98,31 +92,30 @@ export class PeopleComponent extends NewBasePeopleComponent user.email) ?? [], + allOrganizationUserEmails: this.dataSource.data?.map((user) => user.email) ?? [], usesKeyConnector: user?.usesKeyConnector, isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone, initialTab: initialTab, - numConfirmedMembers: this.confirmedCount, + numConfirmedMembers: this.dataSource.confirmedUserCount, }, }); const result = await lastValueFrom(dialog.closed); switch (result) { case MemberDialogResult.Deleted: - this.removeUser(user); + this.dataSource.removeUser(user); break; case MemberDialogResult.Saved: case MemberDialogResult.Revoked: @@ -467,7 +503,7 @@ export class PeopleComponent extends NewBasePeopleComponent u.status === OrganizationUserStatusType.Invited); if (filteredUsers.length <= 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("noSelectedUsersApplicable"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); return; } @@ -546,7 +582,7 @@ export class PeopleComponent extends NewBasePeopleComponent !ou.accessSecretsManager); + const users = this.dataSource.getCheckedUsers().filter((ou) => !ou.accessSecretsManager); if (users.length === 0) { - this.platformUtilsService.showToast( - "error", - this.i18nService.t("errorOccurred"), - this.i18nService.t("noSelectedUsersApplicable"), - ); + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("noSelectedUsersApplicable"), + }); return; } @@ -572,7 +608,7 @@ export class PeopleComponent extends NewBasePeopleComponent { + comp.passwordReset.subscribe(() => { modal.close(); // 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 @@ -637,7 +673,7 @@ export class PeopleComponent extends NewBasePeopleComponent import("./members").then((m) => m.MembersModule), }, - { - path: "groups", - component: GroupsComponent, - canActivate: [OrganizationPermissionsGuard], - data: { - titleId: "groups", - organizationPermissions: canAccessGroupsTab, + ...featureFlaggedRoute({ + defaultComponent: GroupsComponent, + flaggedComponent: NewGroupsComponent, + featureFlag: FeatureFlag.GroupsComponentRefactor, + routeOptions: { + path: "groups", + canActivate: [organizationPermissionsGuard(canAccessGroupsTab)], + data: { + titleId: "groups", + }, }, - }, + }), { path: "reporting", loadChildren: () => diff --git a/apps/web/src/app/admin-console/organizations/organization.module.ts b/apps/web/src/app/admin-console/organizations/organization.module.ts index 29a7139231e..79f3a8e5f7d 100644 --- a/apps/web/src/app/admin-console/organizations/organization.module.ts +++ b/apps/web/src/app/admin-console/organizations/organization.module.ts @@ -1,3 +1,4 @@ +import { ScrollingModule } from "@angular/cdk/scrolling"; import { NgModule } from "@angular/core"; import { LooseComponentsModule } from "../../shared"; @@ -5,6 +6,7 @@ import { LooseComponentsModule } from "../../shared"; import { CoreOrganizationModule } from "./core"; import { GroupAddEditComponent } from "./manage/group-add-edit.component"; import { GroupsComponent } from "./manage/groups.component"; +import { NewGroupsComponent } from "./manage/new-groups.component"; import { OrganizationsRoutingModule } from "./organization-routing.module"; import { SharedOrganizationModule } from "./shared"; import { AccessSelectorModule } from "./shared/components/access-selector"; @@ -16,7 +18,8 @@ import { AccessSelectorModule } from "./shared/components/access-selector"; CoreOrganizationModule, OrganizationsRoutingModule, LooseComponentsModule, + ScrollingModule, ], - declarations: [GroupsComponent, GroupAddEditComponent], + declarations: [GroupsComponent, NewGroupsComponent, GroupAddEditComponent], }) export class OrganizationModule {} diff --git a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts index ff9caf557ae..14dd708389c 100644 --- a/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/master-password.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder, FormGroup, Validators } from "@angular/forms"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; @@ -21,7 +21,7 @@ export class MasterPasswordPolicy extends BasePolicy { selector: "policy-master-password", templateUrl: "master-password.component.html", }) -export class MasterPasswordPolicyComponent extends BasePolicyComponent { +export class MasterPasswordPolicyComponent extends BasePolicyComponent implements OnInit { MinPasswordLength = Utils.minimumPasswordLength; data: FormGroup> = this.formBuilder.group({ diff --git a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts index dc3b667511b..d84a7dec997 100644 --- a/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/policy-edit.component.ts @@ -1,5 +1,12 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; -import { ChangeDetectorRef, Component, Inject, ViewChild, ViewContainerRef } from "@angular/core"; +import { + AfterViewInit, + ChangeDetectorRef, + Component, + Inject, + ViewChild, + ViewContainerRef, +} from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; @@ -28,7 +35,7 @@ export enum PolicyEditDialogResult { selector: "app-policy-edit", templateUrl: "policy-edit.component.html", }) -export class PolicyEditComponent { +export class PolicyEditComponent implements AfterViewInit { @ViewChild("policyForm", { read: ViewContainerRef, static: true }) policyFormRef: ViewContainerRef; diff --git a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts index fbf338ac1f2..6307ee13fa4 100644 --- a/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/admin-console/organizations/policies/reset-password.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; @@ -22,7 +22,7 @@ export class ResetPasswordPolicy extends BasePolicy { selector: "policy-reset-password", templateUrl: "reset-password.component.html", }) -export class ResetPasswordPolicyComponent extends BasePolicyComponent { +export class ResetPasswordPolicyComponent extends BasePolicyComponent implements OnInit { data = this.formBuilder.group({ autoEnrollEnabled: false, }); diff --git a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts index 1ab386298c3..81837cb8a06 100644 --- a/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/reporting/organization-reporting-routing.module.ts @@ -9,9 +9,9 @@ import { InactiveTwoFactorReportComponent } from "../../../admin-console/organiz import { ReusedPasswordsReportComponent } from "../../../admin-console/organizations/tools/reused-passwords-report.component"; import { UnsecuredWebsitesReportComponent } from "../../../admin-console/organizations/tools/unsecured-websites-report.component"; import { WeakPasswordsReportComponent } from "../../../admin-console/organizations/tools/weak-passwords-report.component"; -import { IsPaidOrgGuard } from "../guards/is-paid-org.guard"; -import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { OrganizationRedirectGuard } from "../guards/org-redirect.guard"; +import { isPaidOrgGuard } from "../guards/is-paid-org.guard"; +import { organizationPermissionsGuard } from "../guards/org-permissions.guard"; +import { organizationRedirectGuard } from "../guards/org-redirect.guard"; import { EventsComponent } from "../manage/events.component"; import { ReportsHomeComponent } from "./reports-home.component"; @@ -19,22 +19,18 @@ import { ReportsHomeComponent } from "./reports-home.component"; const routes: Routes = [ { path: "", - canActivate: [OrganizationPermissionsGuard], - data: { organizationPermissions: canAccessReportingTab }, + canActivate: [organizationPermissionsGuard(canAccessReportingTab)], children: [ { path: "", pathMatch: "full", - canActivate: [OrganizationRedirectGuard], - data: { - autoRedirectCallback: getReportRoute, - }, + canActivate: [organizationRedirectGuard(getReportRoute)], children: [], // This is required to make the auto redirect work, }, { path: "reports", component: ReportsHomeComponent, - canActivate: [OrganizationPermissionsGuard], + canActivate: [organizationPermissionsGuard()], data: { titleId: "reports", }, @@ -45,7 +41,7 @@ const routes: Routes = [ data: { titleId: "exposedPasswordsReport", }, - canActivate: [IsPaidOrgGuard], + canActivate: [isPaidOrgGuard()], }, { path: "inactive-two-factor-report", @@ -53,7 +49,7 @@ const routes: Routes = [ data: { titleId: "inactive2faReport", }, - canActivate: [IsPaidOrgGuard], + canActivate: [isPaidOrgGuard()], }, { path: "reused-passwords-report", @@ -61,7 +57,7 @@ const routes: Routes = [ data: { titleId: "reusedPasswordsReport", }, - canActivate: [IsPaidOrgGuard], + canActivate: [isPaidOrgGuard()], }, { path: "unsecured-websites-report", @@ -69,7 +65,7 @@ const routes: Routes = [ data: { titleId: "unsecuredWebsitesReport", }, - canActivate: [IsPaidOrgGuard], + canActivate: [isPaidOrgGuard()], }, { path: "weak-passwords-report", @@ -77,17 +73,16 @@ const routes: Routes = [ data: { titleId: "weakPasswordsReport", }, - canActivate: [IsPaidOrgGuard], + canActivate: [isPaidOrgGuard()], }, ], }, { path: "events", component: EventsComponent, - canActivate: [OrganizationPermissionsGuard], + canActivate: [organizationPermissionsGuard((org) => org.canAccessEventLogs)], data: { titleId: "eventLogs", - organizationPermissions: (org: Organization) => org.canAccessEventLogs, }, }, ], diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.html b/apps/web/src/app/admin-console/organizations/settings/account.component.html index e34972d36e6..f453546fcad 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.html +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.html @@ -1,9 +1,8 @@ -
@@ -37,9 +36,9 @@

{{ "apiKey" | i18n }}

-

+

{{ "apiKeyDesc" | i18n }} - + {{ "learnMore" | i18n }}

@@ -56,7 +55,7 @@ [formGroup]="collectionManagementFormGroup" >

{{ "collectionManagement" | i18n }}

-

{{ "collectionManagementDesc" | i18n }}

+

{{ "collectionManagementDesc" | i18n }}

{{ "allowAdminAccessToAllCollectionItemsDesc" | i18n }} diff --git a/apps/web/src/app/admin-console/organizations/settings/account.component.ts b/apps/web/src/app/admin-console/organizations/settings/account.component.ts index 82f9a249939..c53a4991d5e 100644 --- a/apps/web/src/app/admin-console/organizations/settings/account.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/account.component.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; import { combineLatest, from, lastValueFrom, of, Subject, switchMap, takeUntil } from "rxjs"; @@ -27,7 +27,7 @@ import { DeleteOrganizationDialogResult, openDeleteOrganizationDialog } from "./ selector: "app-org-account", templateUrl: "account.component.html", }) -export class AccountComponent { +export class AccountComponent implements OnInit, OnDestroy { @ViewChild("apiKeyTemplate", { read: ViewContainerRef, static: true }) apiKeyModalRef: ViewContainerRef; @ViewChild("rotateApiKeyTemplate", { read: ViewContainerRef, static: true }) diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index cc65bef8c7f..4e9180ef123 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -4,8 +4,8 @@ import { RouterModule, Routes } from "@angular/router"; import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { OrganizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; -import { OrganizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; +import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard"; +import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard"; import { PoliciesComponent } from "../../organizations/policies"; import { AccountComponent } from "./account.component"; @@ -14,30 +14,35 @@ import { TwoFactorSetupComponent } from "./two-factor-setup.component"; const routes: Routes = [ { path: "", - canActivate: [OrganizationPermissionsGuard], - data: { organizationPermissions: canAccessSettingsTab }, + canActivate: [organizationPermissionsGuard(canAccessSettingsTab)], children: [ { path: "", pathMatch: "full", - canActivate: [OrganizationRedirectGuard], - data: { - autoRedirectCallback: getSettingsRoute, - }, + canActivate: [organizationRedirectGuard(getSettingsRoute)], children: [], // This is required to make the auto redirect work, }, - { path: "account", component: AccountComponent, data: { titleId: "organizationInfo" } }, + { + path: "account", + component: AccountComponent, + canActivate: [organizationPermissionsGuard((o) => o.isOwner)], + data: { + titleId: "organizationInfo", + }, + }, { path: "two-factor", component: TwoFactorSetupComponent, - data: { titleId: "twoStepLogin" }, + canActivate: [organizationPermissionsGuard((o) => o.use2fa && o.isOwner)], + data: { + titleId: "twoStepLogin", + }, }, { path: "policies", component: PoliciesComponent, - canActivate: [OrganizationPermissionsGuard], + canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)], data: { - organizationPermissions: (org: Organization) => org.canManagePolicies, titleId: "policies", }, }, @@ -48,10 +53,9 @@ const routes: Routes = [ path: "import", loadComponent: () => import("./org-import.component").then((mod) => mod.OrgImportComponent), - canActivate: [OrganizationPermissionsGuard], + canActivate: [organizationPermissionsGuard((org) => org.canAccessImportExport)], data: { titleId: "importData", - organizationPermissions: (org: Organization) => org.canAccessImportExport, }, }, { @@ -60,10 +64,9 @@ const routes: Routes = [ import("../tools/vault-export/org-vault-export.component").then( (mod) => mod.OrganizationVaultExportComponent, ), - canActivate: [OrganizationPermissionsGuard], + canActivate: [organizationPermissionsGuard((org) => org.canAccessImportExport)], data: { titleId: "exportVault", - organizationPermissions: (org: Organization) => org.canAccessImportExport, }, }, ], diff --git a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts index c95ff754c45..39d29741e76 100644 --- a/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/two-factor-setup.component.ts @@ -1,7 +1,8 @@ -import { Component } from "@angular/core"; +import { DialogRef } from "@angular/cdk/dialog"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, takeUntil, map } from "rxjs"; -import { tap } from "rxjs/operators"; +import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; +import { first, tap } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -16,13 +17,14 @@ import { DialogService } from "@bitwarden/components"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; +import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component"; @Component({ selector: "app-two-factor-setup", templateUrl: "../../../auth/settings/two-factor-setup.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { +export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent implements OnInit { tabbedHeader = false; constructor( dialogService: DialogService, @@ -63,23 +65,33 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent { } async manage(type: TwoFactorProviderType) { + // clear any existing subscriptions before creating a new one + this.twoFactorSetupSubscription?.unsubscribe(); + switch (type) { case TwoFactorProviderType.OrganizationDuo: { - const result: AuthResponse = await this.callTwoFactorVerifyDialog( - TwoFactorProviderType.OrganizationDuo, + const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { + data: { type: type, organizationId: this.organizationId }, + }); + const result: AuthResponse = await lastValueFrom( + twoFactorVerifyDialogRef.closed, ); - if (!result) { return; } - - const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); - duoComp.type = TwoFactorProviderType.OrganizationDuo; - duoComp.organizationId = this.organizationId; - duoComp.auth(result); - duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); + const duoComp: DialogRef = TwoFactorDuoComponent.open(this.dialogService, { + data: { + authResponse: result, + organizationId: this.organizationId, + }, }); + this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + duoComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); + }); + break; } default: diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts index e5733e262f6..d390f5c3605 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/accept-family-sponsorship.component.ts @@ -1,5 +1,6 @@ import { Component } from "@angular/core"; import { Params } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { BaseAcceptComponent } from "../../../common/base.accept.component"; @@ -25,9 +26,9 @@ export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/login"], { queryParams: { email: qParams.email } }); } 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.router.navigate([this.registerRoute], { queryParams: { email: qParams.email } }); + // TODO: remove when email verification flag is removed + const registerRoute = await firstValueFrom(this.registerRoute$); + await this.router.navigate([registerRoute], { queryParams: { email: qParams.email } }); } } } diff --git a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts index 2c15ab5ac80..f7ca9b5c75c 100644 --- a/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/admin-console/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -6,6 +6,7 @@ import { first, map, takeUntil } from "rxjs/operators"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { OrganizationSponsorshipRedeemRequest } from "@bitwarden/common/admin-console/models/request/organization/organization-sponsorship-redeem.request"; import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums"; @@ -95,7 +96,12 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { }); this.existingFamilyOrganizations$ = this.organizationService.organizations$.pipe( - map((orgs) => orgs.filter((o) => o.productTierType === ProductTierType.Families)), + map((orgs) => + orgs.filter( + (o) => + o.productTierType === ProductTierType.Families && o.type === OrganizationUserType.Owner, + ), + ), ); this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index cab6189c456..3ea2a8f43e2 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -19,7 +19,10 @@ import { ExposedPasswordsReportComponent as BaseExposedPasswordsReportComponent templateUrl: "../../../tools/reports/pages/exposed-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ExposedPasswordsReportComponent extends BaseExposedPasswordsReportComponent { +export class ExposedPasswordsReportComponent + extends BaseExposedPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts index abfbd45f38a..6eb8889dc2f 100644 --- a/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/inactive-two-factor-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,7 +18,10 @@ import { InactiveTwoFactorReportComponent as BaseInactiveTwoFactorReportComponen templateUrl: "../../../tools/reports/pages/inactive-two-factor-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class InactiveTwoFactorReportComponent extends BaseInactiveTwoFactorReportComponent { +export class InactiveTwoFactorReportComponent + extends BaseInactiveTwoFactorReportComponent + implements OnInit +{ constructor( cipherService: CipherService, modalService: ModalService, diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 76d783b6664..0fea1f0a584 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -18,7 +18,10 @@ import { ReusedPasswordsReportComponent as BaseReusedPasswordsReportComponent } templateUrl: "../../../tools/reports/pages/reused-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class ReusedPasswordsReportComponent extends BaseReusedPasswordsReportComponent { +export class ReusedPasswordsReportComponent + extends BaseReusedPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts index 7f6f08fb96f..559d2f417a5 100644 --- a/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/unsecured-websites-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -17,7 +17,10 @@ import { UnsecuredWebsitesReportComponent as BaseUnsecuredWebsitesReportComponen templateUrl: "../../../tools/reports/pages/unsecured-websites-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class UnsecuredWebsitesReportComponent extends BaseUnsecuredWebsitesReportComponent { +export class UnsecuredWebsitesReportComponent + extends BaseUnsecuredWebsitesReportComponent + implements OnInit +{ constructor( cipherService: CipherService, modalService: ModalService, diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index 0ac21294782..06e6fdf0a75 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -1,4 +1,4 @@ -import { Component } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; @@ -19,7 +19,10 @@ import { WeakPasswordsReportComponent as BaseWeakPasswordsReportComponent } from templateUrl: "../../../tools/reports/pages/weak-passwords-report.component.html", }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class WeakPasswordsReportComponent extends BaseWeakPasswordsReportComponent { +export class WeakPasswordsReportComponent + extends BaseWeakPasswordsReportComponent + implements OnInit +{ manageableCiphers: Cipher[]; constructor( diff --git a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts index b228a4d135e..e262fa51ffe 100644 --- a/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts +++ b/apps/web/src/app/admin-console/organizations/users/enroll-master-password-reset.component.ts @@ -2,6 +2,8 @@ import { UserVerificationDialogComponent } from "@bitwarden/auth/angular"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserResetPasswordEnrollmentRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { VerificationWithSecret } from "@bitwarden/common/auth/types/verification"; 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 +28,7 @@ export class EnrollMasterPasswordReset { i18nService: I18nService, syncService: SyncService, logService: LogService, + userVerificationService: UserVerificationService, ) { const result = await UserVerificationDialogComponent.open(dialogService, { title: "enrollAccountRecovery", @@ -33,36 +36,42 @@ export class EnrollMasterPasswordReset { text: "resetPasswordEnrollmentWarning", type: "warning", }, + verificationType: { + type: "custom", + verificationFn: async (secret: VerificationWithSecret) => { + const request = + await userVerificationService.buildRequest( + secret, + ); + request.resetPasswordKey = await resetPasswordService.buildRecoveryKey( + data.organization.id, + ); + + // Process the enrollment request, which is an endpoint that is + // gated by a server-side check of the master password hash + await organizationUserService.putOrganizationUserResetPasswordEnrollment( + data.organization.id, + data.organization.userId, + request, + ); + return true; + }, + }, }); - // Handle the result of the dialog based on user action and verification success + // User canceled enrollment if (result.userAction === "cancel") { return; } - // User confirmed the dialog so check verification success + // Enrollment failed if (!result.verificationSuccess) { - // verification failed return; } - // Verification succeeded + // Enrollment succeeded try { - // This object is missing most of the properties in the - // `OrganizationUserResetPasswordEnrollmentRequest()`, but those - // properties don't carry over to the server model anyway and are - // never used by this flow. - const request = new OrganizationUserResetPasswordEnrollmentRequest(); - request.resetPasswordKey = await resetPasswordService.buildRecoveryKey(data.organization.id); - - await organizationUserService.putOrganizationUserResetPasswordEnrollment( - data.organization.id, - data.organization.userId, - request, - ); - platformUtilsService.showToast("success", null, i18nService.t("enrollPasswordResetSuccess")); - await syncService.fullSync(true); } catch (e) { logService.error(e); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index c9fbf359f0f..1c5527504d9 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -36,12 +36,12 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { StateEventRunnerService } from "@bitwarden/common/platform/state"; import { SyncService } from "@bitwarden/common/platform/sync"; -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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { DialogService, ToastOptions, ToastService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { PolicyListService } from "./admin-console/core/policy-list.service"; import { @@ -338,6 +338,7 @@ export class AppComponent implements OnDestroy, OnInit { this.authService.logOut(async () => { await this.stateService.clean({ userId: userId }); await this.accountService.clean(userId); + await this.accountService.switchAccount(null); await logoutPromise; diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index 4ef20f4b97d..6a28efcbaad 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -1 +1,2 @@ export * from "./webauthn-login"; +export * from "./registration"; diff --git a/apps/web/src/app/auth/core/services/registration/index.ts b/apps/web/src/app/auth/core/services/registration/index.ts new file mode 100644 index 00000000000..6d5565e7e30 --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/index.ts @@ -0,0 +1 @@ +export * from "./web-registration-finish.service"; diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts new file mode 100644 index 00000000000..999e603ef6a --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.spec.ts @@ -0,0 +1,241 @@ +import { MockProxy, mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { PasswordInputResult } from "@bitwarden/auth/angular"; +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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { DEFAULT_KDF_CONFIG } 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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; +import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; +import { CsprngArray } from "@bitwarden/common/types/csprng"; +import { MasterKey, UserKey } from "@bitwarden/common/types/key"; + +import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; +import { OrganizationInvite } from "../../../organization-invite/organization-invite"; + +import { WebRegistrationFinishService } from "./web-registration-finish.service"; + +describe("DefaultRegistrationFinishService", () => { + let service: WebRegistrationFinishService; + + let cryptoService: MockProxy; + let accountApiService: MockProxy; + let acceptOrgInviteService: MockProxy; + let policyApiService: MockProxy; + let logService: MockProxy; + let policyService: MockProxy; + + beforeEach(() => { + cryptoService = mock(); + accountApiService = mock(); + acceptOrgInviteService = mock(); + policyApiService = mock(); + logService = mock(); + policyService = mock(); + + service = new WebRegistrationFinishService( + cryptoService, + accountApiService, + acceptOrgInviteService, + policyApiService, + logService, + policyService, + ); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getMasterPasswordPolicyOptsFromOrgInvite()", () => { + let orgInvite: OrganizationInvite | null; + + beforeEach(() => { + orgInvite = new OrganizationInvite(); + orgInvite.organizationId = "organizationId"; + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + orgInvite.email = "email"; + }); + + it("returns null when the org invite is null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + }); + + it("returns null when the policies are null", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockResolvedValue(null); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + }); + + it("logs an error and returns null when policies cannot be fetched", async () => { + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockRejectedValue(new Error("error")); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toBeNull(); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + expect(logService.error).toHaveBeenCalled(); + }); + + it("returns the master password policy options from the organization invite when it exists", async () => { + const masterPasswordPolicies = [new Policy()]; + const masterPasswordPolicyOptions = new MasterPasswordPolicyOptions(); + + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + policyApiService.getPoliciesByToken.mockResolvedValue(masterPasswordPolicies); + policyService.masterPasswordPolicyOptions$.mockReturnValue(of(masterPasswordPolicyOptions)); + + const result = await service.getMasterPasswordPolicyOptsFromOrgInvite(); + + expect(result).toEqual(masterPasswordPolicyOptions); + expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled(); + expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + }); + }); + + describe("finishRegistration()", () => { + let email: string; + let emailVerificationToken: string; + let masterKey: MasterKey; + let passwordInputResult: PasswordInputResult; + let userKey: UserKey; + let userKeyEncString: EncString; + let userKeyPair: [string, EncString]; + let capchaBypassToken: string; + + let orgInvite: OrganizationInvite; + + beforeEach(() => { + email = "test@email.com"; + emailVerificationToken = "emailVerificationToken"; + masterKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as MasterKey; + passwordInputResult = { + masterKey: masterKey, + masterKeyHash: "masterKeyHash", + kdfConfig: DEFAULT_KDF_CONFIG, + hint: "hint", + }; + + userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; + userKeyEncString = new EncString("userKeyEncrypted"); + + userKeyPair = ["publicKey", new EncString("privateKey")]; + capchaBypassToken = "capchaBypassToken"; + + orgInvite = new OrganizationInvite(); + orgInvite.organizationUserId = "organizationUserId"; + orgInvite.token = "orgInviteToken"; + }); + + it("throws an error if the user key cannot be created", async () => { + cryptoService.makeUserKey.mockResolvedValue([null, null]); + + await expect(service.finishRegistration(email, passwordInputResult)).rejects.toThrow( + "User key could not be created", + ); + }); + + it("registers the user and returns a captcha bypass token when given valid email verification input", async () => { + cryptoService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + cryptoService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null); + + const result = await service.finishRegistration( + email, + passwordInputResult, + emailVerificationToken, + ); + + expect(result).toEqual(capchaBypassToken); + + expect(cryptoService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: emailVerificationToken, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: undefined, + organizationUserId: undefined, + }), + ); + }); + + it("it registers the user and returns a captcha bypass token when given an org invite", async () => { + cryptoService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]); + cryptoService.makeKeyPair.mockResolvedValue(userKeyPair); + accountApiService.registerFinish.mockResolvedValue(capchaBypassToken); + acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite); + + const result = await service.finishRegistration(email, passwordInputResult); + + expect(result).toEqual(capchaBypassToken); + + expect(cryptoService.makeUserKey).toHaveBeenCalledWith(masterKey); + expect(cryptoService.makeKeyPair).toHaveBeenCalledWith(userKey); + expect(accountApiService.registerFinish).toHaveBeenCalledWith( + expect.objectContaining({ + email, + emailVerificationToken: undefined, + masterPasswordHash: passwordInputResult.masterKeyHash, + masterPasswordHint: passwordInputResult.hint, + userSymmetricKey: userKeyEncString.encryptedString, + userAsymmetricKeys: { + publicKey: userKeyPair[0], + encryptedPrivateKey: userKeyPair[1].encryptedString, + }, + kdf: passwordInputResult.kdfConfig.kdfType, + kdfIterations: passwordInputResult.kdfConfig.iterations, + kdfMemory: undefined, + kdfParallelism: undefined, + orgInviteToken: orgInvite.token, + organizationUserId: orgInvite.organizationUserId, + }), + ); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts new file mode 100644 index 00000000000..b54fd79a947 --- /dev/null +++ b/apps/web/src/app/auth/core/services/registration/web-registration-finish.service.ts @@ -0,0 +1,94 @@ +import { firstValueFrom } from "rxjs"; + +import { + DefaultRegistrationFinishService, + PasswordInputResult, + RegistrationFinishService, +} from "@bitwarden/auth/angular"; +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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; +import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service"; +import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string"; + +import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service"; + +export class WebRegistrationFinishService + extends DefaultRegistrationFinishService + implements RegistrationFinishService +{ + constructor( + protected cryptoService: CryptoService, + protected accountApiService: AccountApiService, + private acceptOrgInviteService: AcceptOrganizationInviteService, + private policyApiService: PolicyApiServiceAbstraction, + private logService: LogService, + private policyService: PolicyService, + ) { + super(cryptoService, accountApiService); + } + + override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise { + // If there's a deep linked org invite, use it to get the password policies + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + + if (orgInvite == null) { + return null; + } + + let policies: Policy[] | null = null; + try { + policies = await this.policyApiService.getPoliciesByToken( + orgInvite.organizationId, + orgInvite.token, + orgInvite.email, + orgInvite.organizationUserId, + ); + } catch (e) { + this.logService.error(e); + } + + if (policies == null) { + return null; + } + + const masterPasswordPolicyOpts: MasterPasswordPolicyOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(policies), + ); + + return masterPasswordPolicyOpts; + } + + // Note: the org invite token and email verification are mutually exclusive. Only one will be present. + override async buildRegisterRequest( + email: string, + emailVerificationToken: string, + passwordInputResult: PasswordInputResult, + encryptedUserKey: EncryptedString, + userAsymmetricKeys: [string, EncString], + ): Promise { + const registerRequest = await super.buildRegisterRequest( + email, + emailVerificationToken, + passwordInputResult, + encryptedUserKey, + userAsymmetricKeys, + ); + + // web specific logic + // Org invites are deep linked. Non-existent accounts are redirected to the register page. + // Org user id and token are included here only for validation and two factor purposes. + const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite(); + if (orgInvite != null) { + registerRequest.organizationUserId = orgInvite.organizationUserId; + registerRequest.orgInviteToken = orgInvite.token; + } + // Invite is accepted after login (on deep link redirect). + + return registerRequest; + } +} diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html index 11491bd5560..315df6f2c83 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.html @@ -29,7 +29,7 @@ diff --git a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts index 378726b8407..d5ca41c42c5 100644 --- a/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts +++ b/apps/web/src/app/auth/emergency-access/accept/accept-emergency.component.ts @@ -1,8 +1,8 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -29,10 +29,10 @@ export class AcceptEmergencyComponent extends BaseAcceptComponent { i18nService: I18nService, route: ActivatedRoute, authService: AuthService, - configService: ConfigService, + registerRouteService: RegisterRouteService, private emergencyAccessService: EmergencyAccessService, ) { - super(router, platformUtilsService, i18nService, route, authService, configService); + super(router, platformUtilsService, i18nService, route, authService, registerRouteService); } async authedHandler(qParams: Params): Promise { diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts index 7241e50d35f..a9727532051 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.spec.ts @@ -3,9 +3,8 @@ import { BehaviorSubject } from "rxjs"; import { OrganizationUserResetPasswordWithIdRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; -import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { WebauthnRotateCredentialRequest } from "@bitwarden/common/auth/models/request/webauthn-rotate-credential.request"; -import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; @@ -32,6 +31,7 @@ import { UserKeyRotationService } from "./user-key-rotation.service"; describe("KeyRotationService", () => { let keyRotationService: UserKeyRotationService; + let mockUserVerificationService: MockProxy; let mockApiService: MockProxy; let mockCipherService: MockProxy; let mockFolderService: MockProxy; @@ -42,10 +42,8 @@ describe("KeyRotationService", () => { let mockCryptoService: MockProxy; let mockEncryptService: MockProxy; let mockConfigService: MockProxy; - let mockKdfConfigService: MockProxy; let mockSyncService: MockProxy; let mockWebauthnLoginAdminService: MockProxy; - let mockMasterPasswordService: FakeMasterPasswordService = new FakeMasterPasswordService(); const mockUser = { id: "mockUserId" as UserId, @@ -55,7 +53,7 @@ describe("KeyRotationService", () => { }; beforeAll(() => { - mockMasterPasswordService = new FakeMasterPasswordService(); + mockUserVerificationService = mock(); mockApiService = mock(); mockCipherService = mock(); mockFolderService = mock(); @@ -66,12 +64,11 @@ describe("KeyRotationService", () => { mockCryptoService = mock(); mockEncryptService = mock(); mockConfigService = mock(); - mockKdfConfigService = mock(); mockSyncService = mock(); mockWebauthnLoginAdminService = mock(); keyRotationService = new UserKeyRotationService( - mockMasterPasswordService, + mockUserVerificationService, mockApiService, mockCipherService, mockFolderService, @@ -81,7 +78,6 @@ describe("KeyRotationService", () => { mockDeviceTrustService, mockCryptoService, mockEncryptService, - mockKdfConfigService, mockSyncService, mockWebauthnLoginAdminService, ); @@ -95,7 +91,6 @@ describe("KeyRotationService", () => { let privateKey: BehaviorSubject; beforeEach(() => { - mockCryptoService.makeMasterKey.mockResolvedValue("mockMasterKey" as any); mockCryptoService.makeUserKey.mockResolvedValue([ new SymmetricCryptoKey(new Uint8Array(64)) as UserKey, { @@ -109,6 +104,12 @@ describe("KeyRotationService", () => { encryptedString: "mockEncryptedData", } as any); + // Mock user verification + mockUserVerificationService.verifyUserByMasterPassword.mockResolvedValue({ + masterKey: "mockMasterKey" as any, + policyOptions: null, + }); + // Mock user key mockCryptoService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); @@ -162,14 +163,6 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("throws if master key creation fails", async () => { - mockCryptoService.makeMasterKey.mockResolvedValueOnce(null); - - await expect( - keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser), - ).rejects.toThrow(); - }); - it("throws if user key creation fails", async () => { mockCryptoService.makeUserKey.mockResolvedValueOnce([null, null]); @@ -186,13 +179,14 @@ describe("KeyRotationService", () => { ).rejects.toThrow(); }); - it("saves the master key in state after creation", async () => { - await keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser); - - expect(mockMasterPasswordService.mock.setMasterKey).toHaveBeenCalledWith( - "mockMasterKey" as any, - mockUser.id, + it("throws if master password is incorrect", async () => { + mockUserVerificationService.verifyUserByMasterPassword.mockRejectedValueOnce( + new Error("Invalid master password"), ); + + await expect( + keyRotationService.rotateUserKeyAndEncryptedData("mockMasterPassword", mockUser), + ).rejects.toThrow(); }); it("throws if server rotation fails", async () => { diff --git a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts index 0453b10cbae..f2ec25d9c5b 100644 --- a/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts +++ b/apps/web/src/app/auth/key-rotation/user-key-rotation.service.ts @@ -3,8 +3,9 @@ import { firstValueFrom } from "rxjs"; import { AccountInfo } from "@bitwarden/common/auth/abstractions/account.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 { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; +import { MasterPasswordVerification } from "@bitwarden/common/auth/types/verification"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service"; import { EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string"; @@ -25,7 +26,7 @@ import { UserKeyRotationApiService } from "./user-key-rotation-api.service"; @Injectable() export class UserKeyRotationService { constructor( - private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private userVerificationService: UserVerificationService, private apiService: UserKeyRotationApiService, private cipherService: CipherService, private folderService: FolderService, @@ -35,7 +36,6 @@ export class UserKeyRotationService { private deviceTrustService: DeviceTrustServiceAbstraction, private cryptoService: CryptoService, private encryptService: EncryptService, - private kdfConfigService: KdfConfigService, private syncService: SyncService, private webauthnLoginAdminService: WebauthnLoginAdminService, ) {} @@ -58,19 +58,19 @@ export class UserKeyRotationService { ); } - // Create master key to validate the master password - const masterKey = await this.cryptoService.makeMasterKey( - masterPassword, + // Verify master password + // UV service sets master key on success since it is stored in memory and can be lost on refresh + const verification = { + type: VerificationType.MasterPassword, + secret: masterPassword, + } as MasterPasswordVerification; + + const { masterKey } = await this.userVerificationService.verifyUserByMasterPassword( + verification, + user.id, user.email, - await this.kdfConfigService.getKdfConfig(), ); - if (!masterKey) { - throw new Error("Master key could not be created"); - } - - // Set master key again in case it was lost (could be lost on refresh) - await this.masterPasswordService.setMasterKey(masterKey, user.id); const [newUserKey, newEncUserKey] = await this.cryptoService.makeUserKey(masterKey); if (!newUserKey || !newEncUserKey) { diff --git a/apps/web/src/app/auth/lock.component.html b/apps/web/src/app/auth/lock.component.html index 2cdc4d2a651..f630906223b 100644 --- a/apps/web/src/app/auth/lock.component.html +++ b/apps/web/src/app/auth/lock.component.html @@ -1,66 +1,27 @@ -
-
-
-

- -

-

{{ "yourVaultIsLocked" | i18n }}

-
-
-
- -
- - -
- - {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} - -
-
-
- - -
-
-
-
+ + + {{ "masterPass" | i18n }} + + + {{ "loggedInAsEmailOn" | i18n: email : webVaultHostname }} + + +
+ +
+ +
diff --git a/apps/web/src/app/auth/lock.component.ts b/apps/web/src/app/auth/lock.component.ts index 021bf0f9df4..6b52d0160b8 100644 --- a/apps/web/src/app/auth/lock.component.ts +++ b/apps/web/src/app/auth/lock.component.ts @@ -1,18 +1,49 @@ -import { Component } from "@angular/core"; +import { Component, inject } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; +import { SharedModule } from "../shared"; + @Component({ selector: "app-lock", templateUrl: "lock.component.html", + standalone: true, + imports: [SharedModule], }) export class LockComponent extends BaseLockComponent { + formBuilder = inject(FormBuilder); + + formGroup = this.formBuilder.group({ + masterPassword: ["", { validators: Validators.required, updateOn: "submit" }], + }); + + get masterPasswordFormControl() { + return this.formGroup.controls.masterPassword; + } + async ngOnInit() { await super.ngOnInit(); + + this.masterPasswordFormControl.setValue(this.masterPassword); + this.onSuccessfulSubmit = 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.router.navigateByUrl(this.successRoute); + await this.router.navigateByUrl(this.successRoute); }; } + + async superSubmit() { + await super.submit(); + } + + submit = async () => { + this.formGroup.markAllAsTouched(); + + if (this.formGroup.invalid) { + return; + } + + this.masterPassword = this.masterPasswordFormControl.value; + await this.superSubmit(); + }; } diff --git a/apps/web/src/app/auth/login/login.component.html b/apps/web/src/app/auth/login/login.component.html index f54153be6ae..64f1e80a990 100644 --- a/apps/web/src/app/auth/login/login.component.html +++ b/apps/web/src/app/auth/login/login.component.html @@ -56,7 +56,7 @@ clicking on the link. Mousedown fires before onBlur. -->
{{ "createAccount" | i18n }}{{ "getMasterPasswordHint" | i18n }}
diff --git a/apps/web/src/app/auth/login/login.component.ts b/apps/web/src/app/auth/login/login.component.ts index a3ff8bef436..2ff81c02b4e 100644 --- a/apps/web/src/app/auth/login/login.component.ts +++ b/apps/web/src/app/auth/login/login.component.ts @@ -1,7 +1,7 @@ import { Component, NgZone, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { takeUntil } from "rxjs"; +import { firstValueFrom, takeUntil } from "rxjs"; import { first } from "rxjs/operators"; import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component"; @@ -9,6 +9,7 @@ import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstrac import { LoginStrategyServiceAbstraction, LoginEmailServiceAbstraction, + RegisterRouteService, } from "@bitwarden/auth/common"; 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"; @@ -20,14 +21,13 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/ import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { flagEnabled } from "../../../utils/flags"; import { RouterService, StateService } from "../../core"; @@ -68,7 +68,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { loginEmailService: LoginEmailServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, webAuthnLoginService: WebAuthnLoginServiceAbstraction, - configService: ConfigService, + registerRouteService: RegisterRouteService, ) { super( devicesApiService, @@ -89,7 +89,7 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { loginEmailService, ssoLoginService, webAuthnLoginService, - configService, + registerRouteService, ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; this.showPasswordless = flagEnabled("showPasswordless"); @@ -160,19 +160,22 @@ export class LoginComponent extends BaseLoginComponent implements OnInit { } async goToHint() { - this.setLoginEmailValues(); + await this.saveEmailSettings(); await this.router.navigateByUrl("/hint"); } async goToRegister() { + // TODO: remove when email verification flag is removed + const registerRoute = await firstValueFrom(this.registerRoute$); + if (this.emailFormControl.valid) { - await this.router.navigate([this.registerRoute], { + await this.router.navigate([registerRoute], { queryParams: { email: this.emailFormControl.value }, }); return; } - await this.router.navigate([this.registerRoute]); + await this.router.navigate([registerRoute]); } protected override async handleMigrateEncryptionKey(result: AuthResult): Promise { diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.html b/apps/web/src/app/auth/organization-invite/accept-organization.component.html index 6e88d03ffae..04258e7a46a 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.html +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.html @@ -32,7 +32,7 @@ {{ "logIn" | i18n }} diff --git a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts index 7326c5a5b56..6013688df22 100644 --- a/apps/web/src/app/auth/organization-invite/accept-organization.component.ts +++ b/apps/web/src/app/auth/organization-invite/accept-organization.component.ts @@ -1,8 +1,9 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Params, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; +import { RegisterRouteService } from "@bitwarden/auth/common"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -24,10 +25,10 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { i18nService: I18nService, route: ActivatedRoute, authService: AuthService, - configService: ConfigService, + registerRouteService: RegisterRouteService, private acceptOrganizationInviteService: AcceptOrganizationInviteService, ) { - super(router, platformUtilsService, i18nService, route, authService, configService); + super(router, platformUtilsService, i18nService, route, authService, registerRouteService); } async authedHandler(qParams: Params): Promise { @@ -91,22 +92,23 @@ export class AcceptOrganizationComponent extends BaseAcceptComponent { // TODO: update logic when email verification flag is removed let queryParams: Params; - if (this.registerRoute === "/register") { + let registerRoute = await firstValueFrom(this.registerRoute$); + if (registerRoute === "/register") { queryParams = { fromOrgInvite: "true", email: invite.email, }; - } else if (this.registerRoute === "/signup") { + } else if (registerRoute === "/signup") { // We have to override the base component route b/c it is correct for other components // that extend the base accept comp. We don't need users to complete email verification // if they are coming directly from an emailed org invite. - this.registerRoute = "/finish-signup"; + registerRoute = "/finish-signup"; queryParams = { email: invite.email, }; } - await this.router.navigate([this.registerRoute], { + await this.router.navigate([registerRoute], { queryParams: queryParams, }); return; diff --git a/apps/web/src/app/auth/register-form/register-form.component.ts b/apps/web/src/app/auth/register-form/register-form.component.ts index 4532cf14050..ce1255e023a 100644 --- a/apps/web/src/app/auth/register-form/register-form.component.ts +++ b/apps/web/src/app/auth/register-form/register-form.component.ts @@ -17,8 +17,8 @@ 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 { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { AcceptOrganizationInviteService } from "../organization-invite/accept-organization.service"; diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index 95bdb706243..aa27588691f 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -17,11 +17,11 @@ 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 { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; -import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; +import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy"; import { UserKeyRotationService } from "../key-rotation/user-key-rotation.service"; diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html index 2c6a9ba24b8..e38f21fde20 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.html @@ -64,7 +64,7 @@ bitBadge variant="warning" *ngIf="c.status === emergencyAccessStatusType.Accepted" - >{{ "accepted" | i18n }}{{ "needsConfirmation" | i18n }} {{ "accepted" | i18n }}{{ "needsConfirmation" | i18n }} {{ "authenticatorAppTitle" | i18n }} - - Authenticator app logo -

{{ "twoStepAuthenticatorDesc" | i18n }}

-

- 1. {{ "twoStepAuthenticatorDownloadApp" | i18n }} -

-

{{ "twoStepLoginProviderEnabled" | i18n }}

{{ "twoStepAuthenticatorReaddDesc" | i18n }}
- Authenticator app logo

{{ "twoStepAuthenticatorNeedApp" | i18n }}

-
-

{{ "twoStepAuthenticatorAppsRecommended" | i18n }}

-

- 2. {{ "twoStepAuthenticatorScanCode" | i18n }} -

+ {{ "twoStepAuthenticatorInstructionSuffix" | i18n }} +

+ +

+ + Download on App Store + + + + + Get it on Google Play + +

+ {{ "twoStepAuthenticatorScanCodeV2" | i18n }} +

-

-
+

+ + +

+ {{ "twoStepAuthenticatorQRCanvasError" | i18n }} +

+ + +
{{ key }}

- - - 3. {{ "twoStepAuthenticatorEnterCode" | i18n }} - - - + + {{ "twoStepAuthenticatorEnterCodeV2" | i18n }} + + - - diff --git a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts index 17cdbb595f7..7672ef70281 100644 --- a/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-authenticator.component.ts @@ -1,6 +1,6 @@ import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, EventEmitter, Inject, OnDestroy, OnInit, Output } from "@angular/core"; -import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { FormBuilder, FormControl, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -43,13 +43,13 @@ export class TwoFactorAuthenticatorComponent @Output() onChangeStatus = new EventEmitter(); type = TwoFactorProviderType.Authenticator; key: string; - formPromise: Promise; override componentName = "app-two-factor-authenticator"; + qrScriptError = false; private qrScript: HTMLScriptElement; - protected formGroup = new FormGroup({ - token: new FormControl(null, [Validators.required]), + formGroup = this.formBuilder.group({ + token: new FormControl(null, [Validators.required, Validators.minLength(6)]), }); constructor( @@ -58,6 +58,7 @@ export class TwoFactorAuthenticatorComponent apiService: ApiService, i18nService: I18nService, userVerificationService: UserVerificationService, + private formBuilder: FormBuilder, platformUtilsService: PlatformUtilsService, logService: LogService, private accountService: AccountService, @@ -85,67 +86,112 @@ export class TwoFactorAuthenticatorComponent window.document.body.removeChild(this.qrScript); } - auth(authResponse: AuthResponse) { + validateTokenControl() { + this.formGroup.controls.token.markAsTouched(); + } + + async auth(authResponse: AuthResponse) { super.auth(authResponse); return this.processResponse(authResponse.response); } submit = async () => { + if (this.formGroup.invalid && !this.enabled) { + return; + } if (this.enabled) { - await this.disableAuthentication(this.formPromise); - this.onChangeStatus.emit(this.enabled); - this.close(); + await this.disableMethod(); + this.dialogRef.close(this.enabled); } else { await this.enable(); - this.onChangeStatus.emit(this.enabled); } + this.onChangeStatus.emit(this.enabled); }; - private async disableAuthentication(promise: Promise) { - return super.disable(promise); - } - protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorAuthenticatorRequest); request.token = this.formGroup.value.token; request.key = this.key; - return super.enable(async () => { - this.formPromise = this.apiService.putTwoFactorAuthenticator(request); - const response = await this.formPromise; - await this.processResponse(response); - }); + const response = await this.apiService.putTwoFactorAuthenticator(request); + await this.processResponse(response); + this.onUpdated.emit(true); } private async processResponse(response: TwoFactorAuthenticatorResponse) { this.formGroup.get("token").setValue(null); this.enabled = response.enabled; this.key = response.key; + + await this.waitForQRiousToLoadOrError().catch((error) => { + this.logService.error(error); + this.qrScriptError = true; + }); + + await this.createQRCode(); + } + + private async waitForQRiousToLoadOrError(): Promise { + // Check if QRious is already loaded or if there was an error loading it either way don't wait for it to try and load again + if (typeof window.QRious !== "undefined" || this.qrScriptError) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + this.qrScript.onload = () => resolve(); + this.qrScript.onerror = () => + reject(new Error(this.i18nService.t("twoStepAuthenticatorQRCanvasError"))); + }); + } + + private async createQRCode() { + if (this.qrScriptError) { + return; + } const email = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.email)), ); - window.setTimeout(() => { - new window.QRious({ - element: document.getElementById("qr"), - value: - "otpauth://totp/Bitwarden:" + - Utils.encodeRFC3986URIComponent(email) + - "?secret=" + - encodeURIComponent(this.key) + - "&issuer=Bitwarden", - size: 160, - }); - }, 100); + new window.QRious({ + element: document.getElementById("qr"), + value: + "otpauth://totp/Bitwarden:" + + Utils.encodeRFC3986URIComponent(email) + + "?secret=" + + encodeURIComponent(this.key) + + "&issuer=Bitwarden", + size: 160, + }); } - close = () => { - this.dialogRef.close(this.enabled); - }; - static open( dialogService: DialogService, config: DialogConfig>, ) { return dialogService.open(TwoFactorAuthenticatorComponent, config); } + + async launchExternalUrl(url: string) { + const hostname = new URL(url).hostname; + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("continueToExternalUrlTitle", hostname), + content: this.i18nService.t("continueToExternalUrlDesc"), + type: "info", + acceptButtonText: { key: "continue" }, + }); + if (confirmed) { + this.platformUtilsService.launchUri(url); + } + } + + async launchBitwardenUrl(url: string) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: this.i18nService.t("twoStepContinueToBitwardenUrlTitle"), + content: this.i18nService.t("twoStepContinueToBitwardenUrlDesc"), + type: "info", + acceptButtonText: { key: "continue" }, + }); + if (confirmed) { + this.platformUtilsService.launchUri(url); + } + } } diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.html b/apps/web/src/app/auth/settings/two-factor-duo.component.html index 23ccaf6bde2..6c733ed798a 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.html +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.html @@ -1,98 +1,58 @@ - +
+ + + {{ "twoStepLogin" | i18n }} + Duo + + + + + {{ "twoStepLoginProviderEnabled" | i18n }} + + Duo logo + {{ "twoFactorDuoClientId" | i18n }}: {{ clientId }} +
+ {{ "twoFactorDuoClientSecret" | i18n }}: {{ clientSecret }} +
+ {{ "twoFactorDuoApiHostname" | i18n }}: {{ host }} +
+ + Duo logo +

{{ "twoFactorDuoDesc" | i18n }}

+ + {{ "twoFactorDuoClientId" | i18n }} + + + + {{ "twoFactorDuoClientSecret" | i18n }} + + + + {{ "twoFactorDuoApiHostname" | i18n }} + + +
+
+ + + + +
+
diff --git a/apps/web/src/app/auth/settings/two-factor-duo.component.ts b/apps/web/src/app/auth/settings/two-factor-duo.component.ts index 9a1ec6c4da6..7505fe13b39 100644 --- a/apps/web/src/app/auth/settings/two-factor-duo.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-duo.component.ts @@ -1,4 +1,6 @@ -import { Component } from "@angular/core"; +import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; +import { Component, EventEmitter, Inject, Output } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; @@ -18,21 +20,26 @@ import { TwoFactorBaseComponent } from "./two-factor-base.component"; templateUrl: "two-factor-duo.component.html", }) export class TwoFactorDuoComponent extends TwoFactorBaseComponent { - type = TwoFactorProviderType.Duo; - clientId: string; - clientSecret: string; - host: string; - formPromise: Promise; + @Output() onChangeStatus: EventEmitter = new EventEmitter(); + type = TwoFactorProviderType.Duo; + formGroup = this.formBuilder.group({ + clientId: ["", [Validators.required]], + clientSecret: ["", [Validators.required]], + host: ["", [Validators.required]], + }); override componentName = "app-two-factor-duo"; constructor( + @Inject(DIALOG_DATA) protected data: TwoFactorDuoComponentConfig, apiService: ApiService, i18nService: I18nService, platformUtilsService: PlatformUtilsService, logService: LogService, userVerificationService: UserVerificationService, dialogService: DialogService, + private formBuilder: FormBuilder, + private dialogRef: DialogRef, ) { super( apiService, @@ -44,43 +51,95 @@ export class TwoFactorDuoComponent extends TwoFactorBaseComponent { ); } - auth(authResponse: AuthResponse) { - super.auth(authResponse); - this.processResponse(authResponse.response); + get clientId() { + return this.formGroup.get("clientId").value; + } + get clientSecret() { + return this.formGroup.get("clientSecret").value; + } + get host() { + return this.formGroup.get("host").value; + } + set clientId(value: string) { + this.formGroup.get("clientId").setValue(value); + } + set clientSecret(value: string) { + this.formGroup.get("clientSecret").setValue(value); + } + set host(value: string) { + this.formGroup.get("host").setValue(value); } - submit() { - if (this.enabled) { - return super.disable(this.formPromise); - } else { - return this.enable(); + async ngOnInit() { + if (!this.data?.authResponse) { + throw Error("TwoFactorDuoComponent requires a TwoFactorDuoResponse to initialize"); + } + + super.auth(this.data.authResponse); + this.processResponse(this.data.authResponse.response); + + if (this.data.organizationId) { + this.type = TwoFactorProviderType.OrganizationDuo; + this.organizationId = this.data.organizationId; } } + submit = async () => { + this.formGroup.markAllAsTouched(); + if (this.formGroup.invalid) { + return; + } + if (this.enabled) { + await this.disableMethod(); + } else { + await this.enable(); + } + this.onChangeStatus.emit(this.enabled); + }; + protected async enable() { const request = await this.buildRequestModel(UpdateTwoFactorDuoRequest); request.clientId = this.clientId; request.clientSecret = this.clientSecret; request.host = this.host; - return super.enable(async () => { - if (this.organizationId != null) { - this.formPromise = this.apiService.putTwoFactorOrganizationDuo( - this.organizationId, - request, - ); - } else { - this.formPromise = this.apiService.putTwoFactorDuo(request); - } - const response = await this.formPromise; - await this.processResponse(response); - }); + let response: TwoFactorDuoResponse; + + if (this.organizationId != null) { + response = await this.apiService.putTwoFactorOrganizationDuo(this.organizationId, request); + } else { + response = await this.apiService.putTwoFactorDuo(request); + } + + this.processResponse(response); + this.onUpdated.emit(true); } + onClose = () => { + this.dialogRef.close(this.enabled); + }; + private processResponse(response: TwoFactorDuoResponse) { this.clientId = response.clientId; this.clientSecret = response.clientSecret; this.host = response.host; this.enabled = response.enabled; } + + /** + * Strongly typed helper to open a TwoFactorDuoComponentComponent + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ + static open = ( + dialogService: DialogService, + config: DialogConfig, + ) => { + return dialogService.open(TwoFactorDuoComponent, config); + }; } + +type TwoFactorDuoComponentConfig = { + authResponse: AuthResponse; + organizationId?: string; +}; diff --git a/apps/web/src/app/auth/settings/two-factor-email.component.ts b/apps/web/src/app/auth/settings/two-factor-email.component.ts index 20ee284be04..c37a5ecada6 100644 --- a/apps/web/src/app/auth/settings/two-factor-email.component.ts +++ b/apps/web/src/app/auth/settings/two-factor-email.component.ts @@ -78,15 +78,15 @@ export class TwoFactorEmailComponent extends TwoFactorBaseComponent { } submit = async () => { + this.formGroup.markAllAsTouched(); + if (this.enabled) { await this.disableEmail(); this.onChangeStatus.emit(false); } else { - this.formGroup.markAllAsTouched(); if (this.formGroup.invalid) { return; } - await this.enable(); this.onChangeStatus.emit(true); } diff --git a/apps/web/src/app/auth/settings/two-factor-setup.component.html b/apps/web/src/app/auth/settings/two-factor-setup.component.html index 28baf72f885..33265e91f78 100644 --- a/apps/web/src/app/auth/settings/two-factor-setup.component.html +++ b/apps/web/src/app/auth/settings/two-factor-setup.component.html @@ -48,11 +48,16 @@
  • - +

    - {{ p.name }} +
    + {{ p.name }} +
    (); private twoFactorAuthPolicyAppliesToActiveUser: boolean; + protected twoFactorSetupSubscription: Subscription; constructor( protected dialogService: DialogService, @@ -126,6 +132,9 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { } async manage(type: TwoFactorProviderType) { + // clear any existing subscriptions before creating a new one + this.twoFactorSetupSubscription?.unsubscribe(); + switch (type) { case TwoFactorProviderType.Authenticator: { const result: AuthResponse = @@ -137,9 +146,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - authComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Authenticator); - }); + this.twoFactorSetupSubscription = authComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + authComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.Authenticator); + }); break; } case TwoFactorProviderType.Yubikey: { @@ -148,11 +160,15 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const yubiComp = await this.openModal(this.yubikeyModalRef, TwoFactorYubiKeyComponent); - yubiComp.auth(result); - yubiComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Yubikey); - }); + const yubiComp: DialogRef = TwoFactorYubiKeyComponent.open( + this.dialogService, + { data: result }, + ); + yubiComp.componentInstance.onUpdated + .pipe(takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + this.updateStatus(enabled, TwoFactorProviderType.Yubikey); + }); break; } case TwoFactorProviderType.Duo: { @@ -161,11 +177,17 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); - duoComp.auth(result); - duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.Duo); + const duoComp: DialogRef = TwoFactorDuoComponent.open(this.dialogService, { + data: { + authResponse: result, + }, }); + this.twoFactorSetupSubscription = duoComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + duoComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.Duo); + }); break; } case TwoFactorProviderType.Email: { @@ -174,12 +196,16 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { if (!result) { return; } - const authComp: DialogRef = TwoFactorEmailComponent.open(this.dialogService, { - data: result, - }); - authComp.componentInstance.onChangeStatus - .pipe(takeUntil(this.destroy$)) + const emailComp: DialogRef = TwoFactorEmailComponent.open( + this.dialogService, + { + data: result, + }, + ); + this.twoFactorSetupSubscription = emailComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) .subscribe((enabled: boolean) => { + emailComp.close(); this.updateStatus(enabled, TwoFactorProviderType.Email); }); break; @@ -194,9 +220,12 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.dialogService, { data: result }, ); - webAuthnComp.componentInstance.onChangeStatus.subscribe((enabled: boolean) => { - this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); - }); + this.twoFactorSetupSubscription = webAuthnComp.componentInstance.onChangeStatus + .pipe(first(), takeUntil(this.destroy$)) + .subscribe((enabled: boolean) => { + webAuthnComp.close(); + this.updateStatus(enabled, TwoFactorProviderType.WebAuthn); + }); break; } default: @@ -239,7 +268,7 @@ export class TwoFactorSetupComponent implements OnInit, OnDestroy { this.modal.close(); } this.providers.forEach((p) => { - if (p.type === type) { + if (p.type === type && enabled !== undefined) { p.enabled = enabled; } }); diff --git a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html index 47482c07a30..098669d7522 100644 --- a/apps/web/src/app/auth/settings/two-factor-yubikey.component.html +++ b/apps/web/src/app/auth/settings/two-factor-yubikey.component.html @@ -1,118 +1,84 @@ -