diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 436e2d4ad0f..7b6d24aa8c0 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -86,9 +86,13 @@ apps/web/src/app/shared @bitwarden/team-platform-dev
apps/web/src/translation-constants.ts @bitwarden/team-platform-dev
# Workflows
.github/workflows/brew-bump-desktop.yml @bitwarden/team-platform-dev
+.github/workflows/build-browser-target.yml @bitwarden/team-platform-dev
.github/workflows/build-browser.yml @bitwarden/team-platform-dev
+.github/workflows/build-cli-target.yml @bitwarden/team-platform-dev
.github/workflows/build-cli.yml @bitwarden/team-platform-dev
+.github/workflows/build-desktop-target.yml @bitwarden/team-platform-dev
.github/workflows/build-desktop.yml @bitwarden/team-platform-dev
+.github/workflows/build-web-target.yml @bitwarden/team-platform-dev
.github/workflows/build-web.yml @bitwarden/team-platform-dev
.github/workflows/chromatic.yml @bitwarden/team-platform-dev
.github/workflows/lint.yml @bitwarden/team-platform-dev
diff --git a/.github/workflows/build-browser-target.yml b/.github/workflows/build-browser-target.yml
new file mode 100644
index 00000000000..3334326920c
--- /dev/null
+++ b/.github/workflows/build-browser-target.yml
@@ -0,0 +1,33 @@
+name: Build Browser on PR Target
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore:
+ - 'l10n_master'
+ - 'cf-pages'
+ paths:
+ - 'apps/browser/**'
+ - 'libs/**'
+ - '*'
+ - '!*.md'
+ - '!*.txt'
+ workflow_call:
+ inputs: {}
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ check-run:
+ name: Check PR run
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+
+ run-workflow:
+ name: Run Build Browser on PR Target
+ needs: check-run
+ if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+ uses: ./.github/workflows/build-browser.yml
+ secrets: inherit
+
diff --git a/.github/workflows/build-browser.yml b/.github/workflows/build-browser.yml
index c9e9f588c83..b9a26f68eeb 100644
--- a/.github/workflows/build-browser.yml
+++ b/.github/workflows/build-browser.yml
@@ -1,7 +1,7 @@
name: Build Browser
on:
- pull_request_target:
+ pull_request:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
@@ -38,19 +38,14 @@ defaults:
shell: bash
jobs:
- check-run:
- name: Check PR run
- uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
-
setup:
name: Setup
runs-on: ubuntu-22.04
- needs:
- - check-run
outputs:
repo_url: ${{ steps.gen_vars.outputs.repo_url }}
adj_build_number: ${{ steps.gen_vars.outputs.adj_build_number }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
+ has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -74,6 +69,14 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
+ - name: Check secrets
+ id: check-secrets
+ env:
+ AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ run: |
+ has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
+
locales-test:
name: Locales Test
@@ -197,22 +200,14 @@ jobs:
npm_command: "dist:edge"
archive_name: "dist-edge.zip"
artifact_name: "dist-edge-MV3"
- - name: "firefox"
- npm_command: "dist:firefox"
- archive_name: "dist-firefox.zip"
- artifact_name: "dist-firefox"
- name: "firefox-mv3"
npm_command: "dist:firefox:mv3"
archive_name: "dist-firefox.zip"
- artifact_name: "DO-NOT-USE-FOR-PROD-dist-firefox-MV3"
- - name: "opera"
- npm_command: "dist:opera"
- archive_name: "dist-opera.zip"
- artifact_name: "dist-opera"
+ artifact_name: "dist-firefox-MV3"
- name: "opera-mv3"
npm_command: "dist:opera:mv3"
archive_name: "dist-opera.zip"
- artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3"
+ artifact_name: "dist-opera-MV3"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -281,6 +276,7 @@ jobs:
needs:
- setup
- locales-test
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
diff --git a/.github/workflows/build-cli-target.yml b/.github/workflows/build-cli-target.yml
new file mode 100644
index 00000000000..81ec4178681
--- /dev/null
+++ b/.github/workflows/build-cli-target.yml
@@ -0,0 +1,33 @@
+name: Build CLI on PR Target
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore:
+ - 'l10n_master'
+ - 'cf-pages'
+ paths:
+ - 'apps/cli/**'
+ - 'libs/**'
+ - '*'
+ - '!*.md'
+ - '!*.txt'
+ - '.github/workflows/build-cli.yml'
+ - 'bitwarden_license/bit-cli/**'
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ check-run:
+ name: Check PR run
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+
+ run-workflow:
+ name: Run Build CLI on PR Target
+ needs: check-run
+ if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+ uses: ./.github/workflows/build-cli.yml
+ secrets: inherit
+
diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml
index d5d78811cda..b3694ac423b 100644
--- a/.github/workflows/build-cli.yml
+++ b/.github/workflows/build-cli.yml
@@ -1,7 +1,7 @@
name: Build CLI
on:
- pull_request_target:
+ pull_request:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
@@ -27,6 +27,8 @@ on:
- '!*.txt'
- '.github/workflows/build-cli.yml'
- 'bitwarden_license/bit-cli/**'
+ workflow_call:
+ inputs: {}
workflow_dispatch:
inputs:
sdk_branch:
@@ -39,18 +41,13 @@ defaults:
working-directory: apps/cli
jobs:
- check-run:
- name: Check PR run
- uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
-
setup:
name: Setup
runs-on: ubuntu-22.04
- needs:
- - check-run
outputs:
package_version: ${{ steps.retrieve-package-version.outputs.package_version }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
+ has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -71,6 +68,14 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
+ - name: Check secrets
+ id: check-secrets
+ env:
+ AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ run: |
+ has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
+
cli:
name: CLI ${{ matrix.os.base }} - ${{ matrix.license_type.readable }}
strategy:
@@ -117,7 +122,7 @@ jobs:
working-directory: ./
- name: Download SDK Artifacts
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -130,7 +135,7 @@ jobs:
if_no_artifact_found: fail
- name: Override SDK
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
@@ -272,7 +277,7 @@ jobs:
working-directory: ./
- name: Download SDK Artifacts
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -285,7 +290,7 @@ jobs:
if_no_artifact_found: fail
- name: Override SDK
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
diff --git a/.github/workflows/build-desktop-target.yml b/.github/workflows/build-desktop-target.yml
new file mode 100644
index 00000000000..8c26f991174
--- /dev/null
+++ b/.github/workflows/build-desktop-target.yml
@@ -0,0 +1,32 @@
+name: Build Desktop on PR Target
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore:
+ - 'l10n_master'
+ - 'cf-pages'
+ paths:
+ - 'apps/desktop/**'
+ - 'libs/**'
+ - '*'
+ - '!*.md'
+ - '!*.txt'
+ - '.github/workflows/build-desktop.yml'
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ check-run:
+ name: Check PR run
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+
+ run-workflow:
+ name: Run Build Desktop on PR Target
+ needs: check-run
+ if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+ uses: ./.github/workflows/build-desktop.yml
+ secrets: inherit
+
diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml
index 73d93d763bb..1b4d2d5a676 100644
--- a/.github/workflows/build-desktop.yml
+++ b/.github/workflows/build-desktop.yml
@@ -26,6 +26,8 @@ on:
- '!*.md'
- '!*.txt'
- '.github/workflows/build-desktop.yml'
+ workflow_call:
+ inputs: {}
workflow_dispatch:
inputs:
sdk_branch:
@@ -38,15 +40,9 @@ defaults:
shell: bash
jobs:
- check-run:
- name: Check PR run
- uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
-
electron-verify:
name: Verify Electron Version
runs-on: ubuntu-22.04
- needs:
- - check-run
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -68,8 +64,6 @@ jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
- needs:
- - check-run
outputs:
package_version: ${{ steps.retrieve-version.outputs.package_version }}
release_channel: ${{ steps.release-channel.outputs.channel }}
@@ -77,6 +71,7 @@ jobs:
rc_branch_exists: ${{ steps.branch-check.outputs.rc_branch_exists }}
hotfix_branch_exists: ${{ steps.branch-check.outputs.hotfix_branch_exists }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
+ has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
defaults:
run:
working-directory: apps/desktop
@@ -139,6 +134,14 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
+ - name: Check secrets
+ id: check-secrets
+ env:
+ AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ run: |
+ has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
+
linux:
name: Linux Build
# Note, before updating the ubuntu version of the workflow, ensure the snap base image
@@ -334,12 +337,14 @@ jobs:
rustup show
- name: Login to Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
@@ -354,7 +359,7 @@ jobs:
working-directory: ./
- name: Download SDK Artifacts
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -367,7 +372,7 @@ jobs:
if_no_artifact_found: fail
- name: Override SDK
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
@@ -387,7 +392,17 @@ jobs:
working-directory: apps/desktop/desktop_native
run: node build.js cross-platform
- - name: Build & Sign (dev)
+ - name: Build
+ run: |
+ npm run build
+
+ - name: Pack
+ if: ${{ needs.setup.outputs.has_secrets == 'false' }}
+ run: |
+ npm run pack:win
+
+ - name: Pack & Sign (dev)
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
env:
ELECTRON_BUILDER_SIGN: 1
SIGNING_VAULT_URL: ${{ steps.retrieve-secrets.outputs.code-signing-vault-url }}
@@ -396,10 +411,10 @@ jobs:
SIGNING_CLIENT_SECRET: ${{ steps.retrieve-secrets.outputs.code-signing-client-secret }}
SIGNING_CERT_NAME: ${{ steps.retrieve-secrets.outputs.code-signing-cert-name }}
run: |
- npm run build
npm run pack:win
- name: Rename appx files for store
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: |
Copy-Item "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx" `
-Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx"
@@ -409,6 +424,7 @@ jobs:
-Destination "./dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx"
- name: Package for Chocolatey
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: |
Copy-Item -Path ./stores/chocolatey -Destination ./dist/chocolatey -Recurse
Copy-Item -Path ./dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe `
@@ -420,6 +436,7 @@ jobs:
choco pack ./dist/chocolatey/bitwarden.nuspec --version "$env:_PACKAGE_VERSION" --out ./dist/chocolatey
- name: Fix NSIS artifact names for auto-updater
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: |
Rename-Item -Path .\dist\nsis-web\Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z `
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
@@ -436,6 +453,7 @@ jobs:
if-no-files-found: error
- name: Upload installer exe artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
@@ -443,6 +461,7 @@ jobs:
if-no-files-found: error
- name: Upload appx ia32 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
@@ -450,6 +469,7 @@ jobs:
if-no-files-found: error
- name: Upload store appx ia32 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
@@ -457,6 +477,7 @@ jobs:
if-no-files-found: error
- name: Upload NSIS ia32 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
@@ -464,6 +485,7 @@ jobs:
if-no-files-found: error
- name: Upload appx x64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
@@ -471,6 +493,7 @@ jobs:
if-no-files-found: error
- name: Upload store appx x64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
@@ -478,6 +501,7 @@ jobs:
if-no-files-found: error
- name: Upload NSIS x64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
@@ -485,6 +509,7 @@ jobs:
if-no-files-found: error
- name: Upload appx ARM64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
@@ -492,6 +517,7 @@ jobs:
if-no-files-found: error
- name: Upload store appx ARM64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
@@ -499,6 +525,7 @@ jobs:
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
@@ -506,6 +533,7 @@ jobs:
if-no-files-found: error
- name: Upload nupkg artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
@@ -513,6 +541,7 @@ jobs:
if-no-files-found: error
- name: Upload auto-update artifact
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: ${{ needs.setup.outputs.release_channel }}.yml
@@ -575,11 +604,13 @@ jobs:
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
- name: Login to Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Download Provisioning Profiles secrets
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
env:
ACCOUNT_NAME: bitwardenci
CONTAINER_NAME: profiles
@@ -597,6 +628,7 @@ jobs:
--output none
- name: Get certificates
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: |
mkdir -p $HOME/certificates
@@ -619,6 +651,7 @@ jobs:
jq -r .value | base64 -d > $HOME/certificates/macdev-cert.p12
- name: Set up keychain
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
@@ -642,11 +675,15 @@ jobs:
security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+ security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
security find-identity -v -p codesigning
- name: Set up provisioning profiles
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: |
cp $HOME/secrets/bitwarden_desktop_appstore.provisionprofile \
$GITHUB_WORKSPACE/apps/desktop/bitwarden_desktop_appstore.provisionprofile
@@ -675,7 +712,7 @@ jobs:
working-directory: ./
- name: Download SDK Artifacts
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -688,7 +725,7 @@ jobs:
if_no_artifact_found: fail
- name: Override SDK
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
@@ -715,6 +752,7 @@ jobs:
browser-build:
name: Browser Build
needs: setup
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: ./.github/workflows/build-browser.yml
secrets: inherit
@@ -722,6 +760,7 @@ jobs:
macos-package-github:
name: MacOS Package GitHub Release Assets
runs-on: macos-13
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
needs:
- browser-build
- macos-build
@@ -974,6 +1013,7 @@ jobs:
macos-package-mas:
name: MacOS Package Prod Release Asset
runs-on: macos-13
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
needs:
- browser-build
- macos-build
@@ -1102,6 +1142,9 @@ jobs:
security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+ security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Set up provisioning profiles
@@ -1253,6 +1296,7 @@ jobs:
macos-package-dev:
name: MacOS Package Dev Release Asset
runs-on: macos-13
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
needs:
- browser-build
- macos-build
@@ -1369,6 +1413,9 @@ jobs:
security import "$HOME/certificates/appstore-installer-cert.p12" -k build.keychain -P "" \
-T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+ security import "$HOME/certificates/macdev-cert.p12" -k build.keychain -P "" \
+ -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild
+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $KEYCHAIN_PASSWORD build.keychain
- name: Set up provisioning profiles
diff --git a/.github/workflows/build-web-target.yml b/.github/workflows/build-web-target.yml
new file mode 100644
index 00000000000..fb7074292b5
--- /dev/null
+++ b/.github/workflows/build-web-target.yml
@@ -0,0 +1,32 @@
+name: Build Web on PR Target
+
+on:
+ pull_request:
+ types: [opened, synchronize]
+ branches-ignore:
+ - 'l10n_master'
+ - 'cf-pages'
+ paths:
+ - 'apps/web/**'
+ - 'libs/**'
+ - '*'
+ - '!*.md'
+ - '!*.txt'
+ - '.github/workflows/build-web.yml'
+
+defaults:
+ run:
+ shell: bash
+
+jobs:
+ check-run:
+ name: Check PR run
+ uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
+
+ run-workflow:
+ name: Run Build Web on PR Target
+ needs: check-run
+ if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
+ uses: ./.github/workflows/build-web.yml
+ secrets: inherit
+
diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml
index b7e8a51897c..423b15372ae 100644
--- a/.github/workflows/build-web.yml
+++ b/.github/workflows/build-web.yml
@@ -1,7 +1,7 @@
name: Build Web
on:
- pull_request_target:
+ pull_request:
types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
@@ -27,6 +27,8 @@ on:
- '.github/workflows/build-web.yml'
release:
types: [published]
+ workflow_call:
+ inputs: {}
workflow_dispatch:
inputs:
custom_tag_extension:
@@ -41,18 +43,13 @@ env:
_AZ_REGISTRY: bitwardenprod.azurecr.io
jobs:
- check-run:
- name: Check PR run
- uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
-
setup:
name: Setup
runs-on: ubuntu-22.04
- needs:
- - check-run
outputs:
version: ${{ steps.version.outputs.value }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
+ has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@@ -70,6 +67,14 @@ jobs:
NODE_VERSION=${NODE_NVMRC/v/''}
echo "node_version=$NODE_VERSION" >> $GITHUB_OUTPUT
+ - name: Check secrets
+ id: check-secrets
+ env:
+ AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
+ run: |
+ has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
+ echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
+
build-artifacts:
name: Build artifacts
runs-on: ubuntu-22.04
@@ -128,7 +133,7 @@ jobs:
run: npm ci
- name: Download SDK Artifacts
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -141,7 +146,7 @@ jobs:
if_no_artifact_found: fail
- name: Override SDK
- if: ${{ inputs.sdk_branch != '' }}
+ if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
@@ -213,19 +218,23 @@ jobs:
########## ACRs ##########
- name: Login to Prod Azure
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }}
- name: Log into Prod container registry
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: az acr login -n bitwardenprod
- name: Login to Azure - CI Subscription
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve github PAT secrets
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
@@ -273,6 +282,7 @@ jobs:
run: echo "name=$_AZ_REGISTRY/${PROJECT_NAME}:${IMAGE_TAG}" >> $GITHUB_OUTPUT
- name: Build Docker image
+ if: ${{ needs.setup.outputs.has_secrets == 'true' }}
id: build-docker
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
with:
@@ -283,7 +293,7 @@ jobs:
tags: ${{ steps.image-name.outputs.name }}
secrets: |
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
-
+
- name: Install Cosign
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 5bc566202c6..4fbef027c7c 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,12 +1,20 @@
name: Lint
on:
- push:
+ pull_request:
+ types: [opened, synchronize]
branches-ignore:
- 'l10n_master'
- 'cf-pages'
paths-ignore:
- '.github/workflows/**'
+ push:
+ branches:
+ - 'main'
+ - 'rc'
+ - 'hotfix-rc-*'
+ paths-ignore:
+ - '.github/workflows/**'
workflow_dispatch:
inputs: {}
diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml
index ac7f0ae6f71..c5e189c4666 100644
--- a/.github/workflows/scan.yml
+++ b/.github/workflows/scan.yml
@@ -77,3 +77,4 @@ jobs:
-Dsonar.sources=.
-Dsonar.test.inclusions=**/*.spec.ts
-Dsonar.exclusions=**/*.spec.ts
+ -Dsonar.pullrequest.key=${{ github.event.pull_request.number }}
diff --git a/apps/browser/package.json b/apps/browser/package.json
index 202ec1c4fe1..c37c476bf94 100644
--- a/apps/browser/package.json
+++ b/apps/browser/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
- "version": "2025.1.3",
+ "version": "2025.2.1",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
diff --git a/apps/browser/src/_locales/ar/messages.json b/apps/browser/src/_locales/ar/messages.json
index 269c9d9937a..50442228b7c 100644
--- a/apps/browser/src/_locales/ar/messages.json
+++ b/apps/browser/src/_locales/ar/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "تم إرسال إشعار إلى جهازك."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "بَدْء تسجيل الدخول"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "كلمة المرور الرئيسية مكشوفة"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/az/messages.json b/apps/browser/src/_locales/az/messages.json
index e06f7117e49..5361782b104 100644
--- a/apps/browser/src/_locales/az/messages.json
+++ b/apps/browser/src/_locales/az/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Cihazınıza bir bildiriş göndərildi."
},
+ "notificationSentDevicePart1": {
+ "message": "Cihazınızda Bitwarden kilidini açın, ya da "
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "veb tətbiqinizdə"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Təsdiqləməzdən əvvəl Barmaq izi ifadəsinin aşağıdakı ifadə ilə uyuşduğuna əmin olun."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Cihazınıza bir bildiriş göndərildi"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Hesabınızın kilidinin açıq olduğuna və barmaq izi ifadəsinin digər cihazda uyuşduğuna əmin olun"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Tələbiniz təsdiqləndikdə bildiriş alacaqsınız"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Giriş başladıldı"
},
+ "logInRequestSent": {
+ "message": "Tələb göndərildi"
+ },
"exposedMasterPassword": {
"message": "İfşa olunmuş ana parol"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Element adı"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Təşkilat deaktiv edildi"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Ekstra enli"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "\"Yalnız baxma\" icazələrinə sahib kolleksiyaları silə bilməzsiniz: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Lütfən masaüstü tətbiqinizi güncəlləyin"
},
diff --git a/apps/browser/src/_locales/be/messages.json b/apps/browser/src/_locales/be/messages.json
index 04d3ad78982..3c63e22406d 100644
--- a/apps/browser/src/_locales/be/messages.json
+++ b/apps/browser/src/_locales/be/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Апавяшчэнне было адпраўлена на вашу прыладу."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Ініцыяваны ўваход"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Скампраметаваны асноўны пароль"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Назва элемента"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/bg/messages.json b/apps/browser/src/_locales/bg/messages.json
index 1423e64e8a3..d40c4f82e3f 100644
--- a/apps/browser/src/_locales/bg/messages.json
+++ b/apps/browser/src/_locales/bg/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Към устройството Ви е изпратено известие."
},
+ "notificationSentDevicePart1": {
+ "message": "Отключете Битоурден на устройството си или в"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "приложението по уеб"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Уверете се, че уникалната фраза съвпада с тази по-долу, преди да одобрите."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Към устройството Ви е изпратено известие"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Уверете се, че регистрацията Ви е отключена и че уникалната фраза съвпада с другото устройство"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Ще получите уведомление когато заявката бъде одобрена"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Вписването е стартирано"
},
+ "logInRequestSent": {
+ "message": "Заявката е изпратена"
+ },
"exposedMasterPassword": {
"message": "Разобличена главна парола"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Име на елемента"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Организацията е деактивирана"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Много широко"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Не можете да премахвате колекции с права „Само за преглед“: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Моля, обновете самостоятелното приложение"
},
diff --git a/apps/browser/src/_locales/bn/messages.json b/apps/browser/src/_locales/bn/messages.json
index a4459b177e4..6de7161004b 100644
--- a/apps/browser/src/_locales/bn/messages.json
+++ b/apps/browser/src/_locales/bn/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/bs/messages.json b/apps/browser/src/_locales/bs/messages.json
index f826d33ae35..f2c3c1c0002 100644
--- a/apps/browser/src/_locales/bs/messages.json
+++ b/apps/browser/src/_locales/bs/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/ca/messages.json b/apps/browser/src/_locales/ca/messages.json
index ca168332c12..cb5fc9fb61a 100644
--- a/apps/browser/src/_locales/ca/messages.json
+++ b/apps/browser/src/_locales/ca/messages.json
@@ -879,7 +879,7 @@
"message": "Enllaç caducat"
},
"pleaseRestartRegistrationOrTryLoggingIn": {
- "message": "Please restart registration or try logging in."
+ "message": "Reinicieu el registre o proveu d'iniciar sessió."
},
"youMayAlreadyHaveAnAccount": {
"message": "És possible que ja tingueu un compte"
@@ -2062,7 +2062,7 @@
"message": "Generador de nom d'usuari"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Utilitza aquest correu"
},
"useThisPassword": {
"message": "Utilitzeu aquesta contrasenya"
@@ -2826,7 +2826,7 @@
"message": "Error de desxifrat"
},
"couldNotDecryptVaultItemsBelow": {
- "message": "Bitwarden could not decrypt the vault item(s) listed below."
+ "message": "Bitwarden no ha pogut desxifrar els elements de la caixa forta que s'indiquen a continuació."
},
"contactCSToAvoidDataLossPart1": {
"message": "Contacteu amb el servei d'atenció al client",
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "S'ha enviat una notificació al vostre dispositiu."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "S'ha enviat una notificació al vostre dispositiu"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Assegureu-vos que la vostra caixa forta estiga desbloquejada i que la frase d'empremta digital coincidisca en l'altre dispositiu"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Se us notificarà un vegada s'haja aprovat la sol·licitud"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "S'ha iniciat la sessió"
},
+ "logInRequestSent": {
+ "message": "Sol·licitud enviada"
+ },
"exposedMasterPassword": {
"message": "Contrasenya mestra exposada"
},
@@ -3917,7 +3926,7 @@
"description": "Label indicating the most common import formats"
},
"confirmContinueToBrowserSettingsTitle": {
- "message": "Continue to browser settings?",
+ "message": "Voleu continuar a la configuració del navegador?",
"description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page"
},
"confirmContinueToHelpCenter": {
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nom d'element"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4184,7 +4184,7 @@
"message": "Enllaçat"
},
"copySuccessful": {
- "message": "Copy Successful"
+ "message": "Còpia correcta"
},
"upload": {
"message": "Puja"
@@ -4196,7 +4196,7 @@
"message": "La mida màxima del fitxer és de 500 MB"
},
"deleteAttachmentName": {
- "message": "Delete attachment $NAME$",
+ "message": "Suprimeix adjunt $NAME$",
"placeholders": {
"name": {
"content": "$1",
@@ -4214,7 +4214,7 @@
}
},
"permanentlyDeleteAttachmentConfirmation": {
- "message": "Are you sure you want to permanently delete this attachment?"
+ "message": "Esteu segur que voleu suprimir definitivament aquest adjunt?"
},
"premium": {
"message": "Premium"
@@ -4511,13 +4511,13 @@
}
},
"successfullyAssignedCollections": {
- "message": "Successfully assigned collections"
+ "message": "Col·leccions assignades correctament"
},
"nothingSelected": {
- "message": "You have not selected anything."
+ "message": "No heu seleccionat res."
},
"movedItemsToOrg": {
- "message": "Selected items moved to $ORGNAME$",
+ "message": "Els elements seleccionats s'han desplaçat a $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4526,7 +4526,7 @@
}
},
"itemsMovedToOrg": {
- "message": "Items moved to $ORGNAME$",
+ "message": "S'han desplaçat elements a $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4535,7 +4535,7 @@
}
},
"itemMovedToOrg": {
- "message": "Item moved to $ORGNAME$",
+ "message": "S'ha desplaçat un element a $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4564,16 +4564,16 @@
"message": "Item Location"
},
"fileSend": {
- "message": "File Send"
+ "message": "Send de fitxer"
},
"fileSends": {
- "message": "File Sends"
+ "message": "Sends de fitxer"
},
"textSend": {
- "message": "Text Send"
+ "message": "Send de text"
},
"textSends": {
- "message": "Text Sends"
+ "message": "Sends de text"
},
"accountActions": {
"message": "Account actions"
@@ -4585,10 +4585,10 @@
"message": "Mostra accions de còpia ràpida a la caixa forta"
},
"systemDefault": {
- "message": "System default"
+ "message": "Per defecte del sistema"
},
"enterprisePolicyRequirementsApplied": {
- "message": "Enterprise policy requirements have been applied to this setting"
+ "message": "Els requisits de la política empresarial s'han aplicat a aquesta configuració"
},
"sshPrivateKey": {
"message": "Clau privada"
@@ -4618,7 +4618,7 @@
"message": "Torneu-ho a provar"
},
"vaultCustomTimeoutMinimum": {
- "message": "Minimum custom timeout is 1 minute."
+ "message": "El temps d'espera personalitzat mínim és d'1 minut."
},
"additionalContentAvailable": {
"message": "Additional content is available"
@@ -4627,10 +4627,10 @@
"message": "File saved to device. Manage from your device downloads."
},
"showCharacterCount": {
- "message": "Show character count"
+ "message": "Mostra el recompte de caràcters"
},
"hideCharacterCount": {
- "message": "Hide character count"
+ "message": "Amaga el recompte de caràcters"
},
"itemsInTrash": {
"message": "Items in trash"
@@ -4642,7 +4642,7 @@
"message": "Items you delete will appear here and be permanently deleted after 30 days"
},
"trashWarning": {
- "message": "Items that have been in trash more than 30 days will automatically be deleted"
+ "message": "Els elements que porten més de 30 dies a la paperera se suprimiran automàticament"
},
"restore": {
"message": "Restaura"
@@ -4651,7 +4651,7 @@
"message": "Suprimeix per sempre"
},
"noEditPermissions": {
- "message": "You don't have permission to edit this item"
+ "message": "No tens permisos per editar aquest fitxer"
},
"biometricsStatusHelptextUnlockNeeded": {
"message": "Biometric unlock is unavailable because PIN or password unlock is required first."
@@ -4688,7 +4688,7 @@
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
- "message": "Password regenerated",
+ "message": "Contrasenya regenerada",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
@@ -4696,7 +4696,7 @@
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
- "message": "Space",
+ "message": "Espai",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
@@ -4708,7 +4708,7 @@
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
- "message": "Exclamation mark",
+ "message": "Signe d'exclamació",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
@@ -4867,13 +4867,13 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
- "message": "No, I do not"
+ "message": "No, jo no"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
},
"turnOnTwoStepLogin": {
- "message": "Turn on two-step login"
+ "message": "Activa l'inici de sessió en dos passos"
},
"changeAcctEmail": {
"message": "Change account email"
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/cs/messages.json b/apps/browser/src/_locales/cs/messages.json
index 9c427edf5cf..025e2028537 100644
--- a/apps/browser/src/_locales/cs/messages.json
+++ b/apps/browser/src/_locales/cs/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Na Vaše zařízení bylo odesláno oznámení."
},
+ "notificationSentDevicePart1": {
+ "message": "Odemknout Bitwarden na Vašem zařízení nebo na"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "webová aplikace"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Před schválením se ujistěte, že fráze otisku prstu odpovídá frázi níže."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Na Vaše zařízení bylo odesláno oznámení"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Ujistěte se, že je Váš trezor odemčen a fráze otisku prstu se shodují s druhým zařízením"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Budete upozorněni, jakmile bude žádost schválena"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Bylo zahájeno přihlášení"
},
+ "logInRequestSent": {
+ "message": "Požadavek odeslán"
+ },
"exposedMasterPassword": {
"message": "Odhalené hlavní heslo"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra široký"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Nemůžete odebrat kolekce s oprávněními jen pro zobrazení: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Aktualizujte aplikaci pro stolní počítač"
},
diff --git a/apps/browser/src/_locales/cy/messages.json b/apps/browser/src/_locales/cy/messages.json
index 31208e2e020..12485e20120 100644
--- a/apps/browser/src/_locales/cy/messages.json
+++ b/apps/browser/src/_locales/cy/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/da/messages.json b/apps/browser/src/_locales/da/messages.json
index b553afdbe2f..9a5e61d6daa 100644
--- a/apps/browser/src/_locales/da/messages.json
+++ b/apps/browser/src/_locales/da/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "En notifikation er sendt til din enhed."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "En notifikation er sendt til enheden"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Sørg for, at boksen er oplåst, samt at fingeraftrykssætningen matcher på den anden enhed"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Man vil få besked, når anmodningen er godkendt"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Indlogning påbegyndt"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Kompromitteret hovedadgangskode"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Ekstra bred"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Samlinger med kun tilladelsen Vis kan ikke fjernes: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Opdatér venligst computerapplikationen"
},
diff --git a/apps/browser/src/_locales/de/messages.json b/apps/browser/src/_locales/de/messages.json
index 1dca0804873..23f35fc931c 100644
--- a/apps/browser/src/_locales/de/messages.json
+++ b/apps/browser/src/_locales/de/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Eine Benachrichtigung wurde an dein Gerät gesendet."
},
+ "notificationSentDevicePart1": {
+ "message": "Entsperre Bitwarden auf deinem Gerät oder mit der"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "Web-App"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Stelle vor der Genehmigung sicher, dass die Fingerabdruck-Phrase mit der unten stehenden übereinstimmt."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Eine Benachrichtigung wurde an dein Gerät gesendet"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Stelle sicher, dass dein Konto entsperrt ist und die Fingerabdruck-Phrase mit der vom anderen Gerät übereinstimmt"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Du wirst benachrichtigt, sobald die Anfrage genehmigt wurde"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Anmeldung eingeleitet"
},
+ "logInRequestSent": {
+ "message": "Anfrage gesendet"
+ },
"exposedMasterPassword": {
"message": "Kompromittiertes Master-Passwort"
},
@@ -3994,7 +4003,7 @@
"message": "Passkey entfernt"
},
"autofillSuggestions": {
- "message": "Vorschläge zum Auto-Ausfüllen"
+ "message": "Auto-Ausfüllen-Vorschläge"
},
"itemSuggestions": {
"message": "Vorgeschlagene Einträge"
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Eintrags-Name"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organisation ist deaktiviert"
},
@@ -4579,7 +4579,7 @@
"message": "Konto-Aktionen"
},
"showNumberOfAutofillSuggestions": {
- "message": "Anzahl der Vorschläge zum Auto-Ausfüllen von Zugangsdaten auf dem Erweiterungssymbol anzeigen"
+ "message": "Anzahl der Auto-Ausfüllen-Vorschläge von Zugangsdaten auf dem Erweiterungssymbol anzeigen"
},
"showQuickCopyActions": {
"message": "Schnellkopier-Aktionen im Tresor anzeigen"
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra breit"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Du kannst Sammlungen mit Leseberechtigung nicht entfernen: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Bitte aktualisiere deine Desktop-Anwendung"
},
diff --git a/apps/browser/src/_locales/el/messages.json b/apps/browser/src/_locales/el/messages.json
index 7d2afbf9969..7c9f6a7740e 100644
--- a/apps/browser/src/_locales/el/messages.json
+++ b/apps/browser/src/_locales/el/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Μια ειδοποίηση έχει σταλεί στη συσκευή σας."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Μια ειδοποίηση στάλθηκε στη συσκευή σας"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Βεβαιωθείτε ότι ο λογαριασμός σας είναι ξεκλείδωτος και ότι η φράση δακτυλικού αποτυπώματος ταιριάζει στην άλλη συσκευή"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Θα ειδοποιηθείτε μόλις εγκριθεί η αίτηση"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Η σύνδεση ξεκίνησε"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Εκτεθειμένος Κύριος Κωδικός Πρόσβασης"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Όνομα αντικειμένου"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Ο οργανισμός απενεργοποιήθηκε"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Εξαιρετικά φαρδύ"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Δεν μπορείτε να αφαιρέσετε συλλογές που έχουν μόνο δικαιώματα Προβολής: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json
index 8698315b57c..7eec2804ece 100644
--- a/apps/browser/src/_locales/en/messages.json
+++ b/apps/browser/src/_locales/en/messages.json
@@ -2363,6 +2363,70 @@
"autofillBlockedNoticeGuidance": {
"message": "Change this in settings"
},
+ "change": {
+ "message": "Change"
+ },
+ "changeButtonTitle": {
+ "message": "Change password - $ITEMNAME$",
+ "placeholders": {
+ "itemname": {
+ "content": "$1",
+ "example": "Secret Item"
+ }
+ }
+ },
+ "atRiskPasswords": {
+ "message": "At-risk passwords"
+ },
+ "atRiskPasswordsDescSingleOrg": {
+ "message": "$ORGANIZATION$ is requesting you change the $COUNT$ passwords because they are at risk.",
+ "placeholders": {
+ "organization": {
+ "content": "$1",
+ "example": "Acme Corp"
+ },
+ "count": {
+ "content": "$2",
+ "example": "2"
+ }
+ }
+ },
+ "atRiskPasswordsDescMultiOrg": {
+ "message": "Your organizations are requesting you change the $COUNT$ passwords because they are at risk.",
+ "placeholders": {
+ "count": {
+ "content": "$1",
+ "example": "2"
+ }
+ }
+ },
+ "reviewAndChangeAtRiskPassword": {
+ "message": "Review and change one at-risk password"
+ },
+ "reviewAndChangeAtRiskPasswordsPlural": {
+ "message": "Review and change $COUNT$ at-risk passwords",
+ "placeholders": {
+ "count": {
+ "content": "$1",
+ "example": "2"
+ }
+ }
+ },
+ "changeAtRiskPasswordsFaster": {
+ "message": "Change at-risk passwords faster"
+ },
+ "changeAtRiskPasswordsFasterDesc": {
+ "message": "Update your settings so you can quickly autofill your passwords and generate new ones"
+ },
+ "turnOnAutofill": {
+ "message": "Turn on autofill"
+ },
+ "turnedOnAutofill": {
+ "message": "Turned on autofill"
+ },
+ "dismiss": {
+ "message": "Dismiss"
+ },
"websiteItemLabel": {
"message": "Website $number$ (URI)",
"placeholders": {
diff --git a/apps/browser/src/_locales/en_GB/messages.json b/apps/browser/src/_locales/en_GB/messages.json
index 6ede9cab724..d591603c420 100644
--- a/apps/browser/src/_locales/en_GB/messages.json
+++ b/apps/browser/src/_locales/en_GB/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/en_IN/messages.json b/apps/browser/src/_locales/en_IN/messages.json
index 1be001abea4..a5bccf2ae83 100644
--- a/apps/browser/src/_locales/en_IN/messages.json
+++ b/apps/browser/src/_locales/en_IN/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/es/messages.json b/apps/browser/src/_locales/es/messages.json
index df5f38f878c..d440e170928 100644
--- a/apps/browser/src/_locales/es/messages.json
+++ b/apps/browser/src/_locales/es/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Se ha enviado una notificación a tu dispositivo."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Inicio de sesión en proceso"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Contraseña maestra comprometida"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nombre del elemento"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "La organización está desactivada"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "No puedes eliminar colecciones con permisos de solo visualización: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/et/messages.json b/apps/browser/src/_locales/et/messages.json
index a5c69ed7cac..30be22a7d6e 100644
--- a/apps/browser/src/_locales/et/messages.json
+++ b/apps/browser/src/_locales/et/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Sinu seadmesse saadeti teavitus."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Sisselogimine on käivitatud"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Ülemparool on haavatav"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/eu/messages.json b/apps/browser/src/_locales/eu/messages.json
index 7f0da4e1d41..4730c5918c6 100644
--- a/apps/browser/src/_locales/eu/messages.json
+++ b/apps/browser/src/_locales/eu/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/fa/messages.json b/apps/browser/src/_locales/fa/messages.json
index 034a79b3d35..8f414b35725 100644
--- a/apps/browser/src/_locales/fa/messages.json
+++ b/apps/browser/src/_locales/fa/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "یک اعلان به دستگاه شما ارسال شده است."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "ورود به سیستم آغاز شد"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "کلمه عبور اصلی افشا شده"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/fi/messages.json b/apps/browser/src/_locales/fi/messages.json
index 8e327cbe222..e06f927072a 100644
--- a/apps/browser/src/_locales/fi/messages.json
+++ b/apps/browser/src/_locales/fi/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Laitteellesi on lähetetty ilmoitus."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Laitteeseesi lähetettiin ilmoitus"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Varmista, että vahvistavan laitteen holvi on avattu ja että se näyttää saman tunnistelausekkeen"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Sinulle ilmoitetaan, kun pyyntö on hyväksytty"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Kirjautuminen aloitettu"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Paljastunut pääsalasana"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Kohteen nimi"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organisaatio on poistettu käytöstä"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Erittäin leveä"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Et voi poistaa kokoelmia, joihin sinulla on vain tarkasteluoikeus: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Päivitä työpöytäsovellus"
},
diff --git a/apps/browser/src/_locales/fil/messages.json b/apps/browser/src/_locales/fil/messages.json
index a37756ece9a..d5f1ee430a5 100644
--- a/apps/browser/src/_locales/fil/messages.json
+++ b/apps/browser/src/_locales/fil/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Naipadala na ang notification sa iyong device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Nakalantad na Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/fr/messages.json b/apps/browser/src/_locales/fr/messages.json
index 26d3c1c352e..a52cb300532 100644
--- a/apps/browser/src/_locales/fr/messages.json
+++ b/apps/browser/src/_locales/fr/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Une notification a été envoyée à votre appareil."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Une notification a été envoyée à votre appareil"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Assurez-vous que votre compte est déverrouillé et que la phrase d'empreinte correspond à l'autre appareil"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Vous serez notifié une fois que la demande sera approuvée"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Connexion initiée"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Mot de passe principal exposé"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nom de l’élément"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "L'organisation est désactivée"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Très large"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Vous ne pouvez pas supprimer des collections avec les autorisations d'affichage uniquement : $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/gl/messages.json b/apps/browser/src/_locales/gl/messages.json
index 423f5f4c471..e92bf5c06b8 100644
--- a/apps/browser/src/_locales/gl/messages.json
+++ b/apps/browser/src/_locales/gl/messages.json
@@ -382,7 +382,7 @@
"message": "Aniñar un cartafol engadindo o nome do cartafol pai seguido dun \"/\". Exemplo: Social/Foros"
},
"noFoldersAdded": {
- "message": "Sen cartafois"
+ "message": "Sen cartafoles"
},
"createFoldersToOrganize": {
"message": "Crea cartafoles para organizar as entradas da túa caixa forte"
@@ -394,10 +394,10 @@
"message": "Eliminar cartafol"
},
"folders": {
- "message": "Cartafois"
+ "message": "Cartafoles"
},
"noFolders": {
- "message": "Non hai cartafois que listar."
+ "message": "Non hai cartafoles que listar."
},
"helpFeedback": {
"message": "Axuda e comentarios"
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Enviouse unha notificación ó teu dispositivo."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Enviouse unha notificación ó teu dispositivo"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Por favor asegúrate de que a sesión está aberta e a frase de pegada dixital coincide ca do outro dispositivo"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Serás notificado unha vez se aprobe a solicitude"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Inicio de sesión comezado"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Contrasinal mestre filtrado"
},
@@ -3810,7 +3819,7 @@
"message": "Autenticación multifactor fallida"
},
"includeSharedFolders": {
- "message": "Incluír cartafois compartidos"
+ "message": "Incluír cartafoles compartidos"
},
"lastPassEmail": {
"message": "Correo de LastPass"
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nome da entrada"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Non podes eliminar coleccións con permisos de Só lectura: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "A organización está desactivada"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Moi ancho"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Non podes eliminar coleccións con permisos de Só lectura: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/he/messages.json b/apps/browser/src/_locales/he/messages.json
index 93600eaf6a9..a5e126046a7 100644
--- a/apps/browser/src/_locales/he/messages.json
+++ b/apps/browser/src/_locales/he/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/hi/messages.json b/apps/browser/src/_locales/hi/messages.json
index 071c7acdb0f..e3a1b690bf3 100644
--- a/apps/browser/src/_locales/hi/messages.json
+++ b/apps/browser/src/_locales/hi/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/hr/messages.json b/apps/browser/src/_locales/hr/messages.json
index a8f9c8b672f..b604d562181 100644
--- a/apps/browser/src/_locales/hr/messages.json
+++ b/apps/browser/src/_locales/hr/messages.json
@@ -446,16 +446,16 @@
"message": "Generiraj frazu lozinke"
},
"passwordGenerated": {
- "message": "Password generated"
+ "message": "Lozinka generirana"
},
"passphraseGenerated": {
- "message": "Passphrase generated"
+ "message": "Frazna lozinka generirana"
},
"usernameGenerated": {
- "message": "Username generated"
+ "message": "Korisničko ime generirano"
},
"emailGenerated": {
- "message": "Email generated"
+ "message": "e-pošta generirana"
},
"regeneratePassword": {
"message": "Ponovno generiraj lozinku"
@@ -660,10 +660,10 @@
"message": "Potvrdi identitet"
},
"weDontRecognizeThisDevice": {
- "message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
+ "message": "Ne prepoznajemo ovaj uređaj. Za potvrdu identiteta unesi kôd poslan e-poštom."
},
"continueLoggingIn": {
- "message": "Continue logging in"
+ "message": "Nastavi prijavu"
},
"yourVaultIsLocked": {
"message": "Tvoj trezor je zaključan. Potvrdi glavnu lozinku za nastavak."
@@ -1005,7 +1005,7 @@
"message": "Pitaj za dodavanje stavke ako nije pronađena u tvojem trezoru. Primjenjuje se na sve prijavljene račune."
},
"showCardsInVaultViewV2": {
- "message": "Always show cards as Autofill suggestions on Vault view"
+ "message": "Uvijek prikaži kartice kao prijedloge za auto-ispunu u prikazu trezora"
},
"showCardsCurrentTab": {
"message": "Prikaži platne kartice"
@@ -1014,7 +1014,7 @@
"message": "Prikazuj platne kartice za jednostavnu auto-ispunu."
},
"showIdentitiesInVaultViewV2": {
- "message": "Always show identities as Autofill suggestions on Vault view"
+ "message": "Uvijek prikaži identitete kao prijedloge za auto-ispunu u prikazu trezora"
},
"showIdentitiesCurrentTab": {
"message": "Prikaži identitete"
@@ -2062,7 +2062,7 @@
"message": "Generator korisničkih imena"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Koristi ovu e-poštu"
},
"useThisPassword": {
"message": "Koristi ovu lozinku"
@@ -2343,7 +2343,7 @@
"description": "A category title describing the concept of web domains"
},
"blockedDomains": {
- "message": "Blocked domains"
+ "message": "Blokirane domene"
},
"excludedDomains": {
"message": "Izuzete domene"
@@ -2355,13 +2355,13 @@
"message": "Bitwarden neće nuditi spremanje podataka za prijavu za ove domene za sve prijavljene račune. Moraš osvježiti stranicu kako bi promjene stupile na snagu."
},
"blockedDomainsDesc": {
- "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
+ "message": "Auto-ispuna i druge vezane značajke neće biti ponuđene za ova web mjesta. Potrebno je osvježiti stranicu zaprimjenu postavki."
},
"autofillBlockedNoticeV2": {
- "message": "Autofill is blocked for this website."
+ "message": "Auto-ispuna je blokirana za ovu web stranicu."
},
"autofillBlockedNoticeGuidance": {
- "message": "Change this in settings"
+ "message": "Promijeni ovo u postavkama"
},
"websiteItemLabel": {
"message": "Web stranica $number$ (URI)",
@@ -2382,7 +2382,7 @@
}
},
"blockedDomainsSavedSuccess": {
- "message": "Blocked domain changes saved"
+ "message": "Spremljene promjene blokiranih domena"
},
"excludedDomainsSavedSuccess": {
"message": "Spremljene promjene izuzete domene"
@@ -2823,17 +2823,17 @@
"message": "Pogreška"
},
"decryptionError": {
- "message": "Decryption error"
+ "message": "Pogreška pri dešifriranju"
},
"couldNotDecryptVaultItemsBelow": {
- "message": "Bitwarden could not decrypt the vault item(s) listed below."
+ "message": "Bitwarden nije mogao dešifrirati sljedeće stavke trezora."
},
"contactCSToAvoidDataLossPart1": {
- "message": "Contact customer success",
+ "message": "Kontaktiraj službu za korisnike",
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"contactCSToAvoidDataLossPart2": {
- "message": "to avoid additional data loss.",
+ "message": "kako bi izbjegli gubitak podataka.",
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"generateUsername": {
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Obavijest je poslana na tvoj uređaj."
},
+ "notificationSentDevicePart1": {
+ "message": "Otključaj Bitwarden na svojem uređaju ili na"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web trezoru"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Provjeri slaže li se jedinstvena fraza s ovdje prikazanom prije odobravanja."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Obavijest je poslana na tvoj uređaj"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Provjeri je li trezor otključan i slaže li se jedinstvena fraza s drugim uređajem"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Dobiti ćeš obavijest kada je tvoj zahtjev odobren"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Prijava pokrenuta"
},
+ "logInRequestSent": {
+ "message": "Zahtjev poslan"
+ },
"exposedMasterPassword": {
"message": "Ukradena glavna lozinka"
},
@@ -3994,10 +4003,10 @@
"message": "Pristupni ključ uklonjen"
},
"autofillSuggestions": {
- "message": "Autofill suggestions"
+ "message": "Prijedlozi auto-ispune"
},
"itemSuggestions": {
- "message": "Suggested items"
+ "message": "Predložene stavke"
},
"autofillSuggestionsTip": {
"message": "Spremi u auto-ispunu stavku prijave za ovu stranicu"
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Naziv stavke"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organizacija je deaktivirana"
},
@@ -4654,22 +4654,22 @@
"message": "Nemaš prava za uređivanje ove stavke"
},
"biometricsStatusHelptextUnlockNeeded": {
- "message": "Biometric unlock is unavailable because PIN or password unlock is required first."
+ "message": "Biometrijsko otključavanje nije dostupno jer je prvo potrebno otključati PIN-om ili lozinkom."
},
"biometricsStatusHelptextHardwareUnavailable": {
- "message": "Biometric unlock is currently unavailable."
+ "message": "Biometrijsko otključavanje trenutno nije dostupno."
},
"biometricsStatusHelptextAutoSetupNeeded": {
- "message": "Biometric unlock is unavailable due to misconfigured system files."
+ "message": "Biometrijsko otključavanje nije dostupno zbog pogrešno konfiguriranih sistemskih datoteka."
},
"biometricsStatusHelptextManualSetupNeeded": {
- "message": "Biometric unlock is unavailable due to misconfigured system files."
+ "message": "Biometrijsko otključavanje nije dostupno zbog pogrešno konfiguriranih sistemskih datoteka."
},
"biometricsStatusHelptextDesktopDisconnected": {
- "message": "Biometric unlock is unavailable because the Bitwarden desktop app is closed."
+ "message": "Biometrijsko otključavanje nije dostupno jer je Bitwarden dekstop aplikacija zatvorena."
},
"biometricsStatusHelptextNotEnabledInDesktop": {
- "message": "Biometric unlock is unavailable because it is not enabled for $EMAIL$ in the Bitwarden desktop app.",
+ "message": "Biometrijsko otključavanje nije dostupno jer nije omogućeno za $EMAIL$ u Bitwarden desktop aplikaciji.",
"placeholders": {
"email": {
"content": "$1",
@@ -4678,7 +4678,7 @@
}
},
"biometricsStatusHelptextUnavailableReasonUnknown": {
- "message": "Biometric unlock is currently unavailable for an unknown reason."
+ "message": "Biometrijsko otključavanje trenutno nije dostupno iz nepoznatog razloga."
},
"authenticating": {
"message": "Autentifikacija"
@@ -4887,10 +4887,19 @@
"extraWide": {
"message": "Ekstra široko"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "S dopuštenjima samo za prikaz ne možeš ukloniti zbirke: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
- "message": "Please update your desktop application"
+ "message": "Molimo, ažuriraj svoju desktop aplikaciju"
},
"updateDesktopAppOrDisableFingerprintDialogMessage": {
- "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings."
+ "message": "Za korištenje biometrijskog otključavanja ažuriraj desktop aplikaciju ili nemogući otključavanje otiskom prsta u desktop aplikaciji."
}
}
diff --git a/apps/browser/src/_locales/hu/messages.json b/apps/browser/src/_locales/hu/messages.json
index 9ac2cea07ce..14f6431b4cb 100644
--- a/apps/browser/src/_locales/hu/messages.json
+++ b/apps/browser/src/_locales/hu/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Egy értesítés lett elküldve az eszközre."
},
+ "notificationSentDevicePart1": {
+ "message": "A Bitwarden zárolás feloldása az eszközön vagy: "
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "webalkalmazás"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Jóváhagyás előtt győződjünk meg arról, hogy az ujjlenyomat kifejezés megegyezik az alábbi kifejezéssel."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Egy értesítés lett elküldve az eszközre."
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Ellenőrizzük, hogy a széf feloldásra került és az ujjlenyomat kifejezés egyezik a másik eszközön levővel."
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "A kérelem jóváhagyása után értesítés érkezik."
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "A bejelentkezés elindításra került."
},
+ "logInRequestSent": {
+ "message": "A kérés elküldésre került."
+ },
"exposedMasterPassword": {
"message": "Kiszivárgott mesterjelszó"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra széles"
},
+ "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"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Frissítsük az asztali alkalmazást."
},
diff --git a/apps/browser/src/_locales/id/messages.json b/apps/browser/src/_locales/id/messages.json
index b059303d5a2..53810e85f77 100644
--- a/apps/browser/src/_locales/id/messages.json
+++ b/apps/browser/src/_locales/id/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Sebuah pemberitahuan dikirim ke perangkat Anda."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Sebuah pemberitahuan telah dikirim ke perangkat Anda"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Pastikan akun Anda terbuka dan frasa sidik jari cocok pada perangkat lainnya"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Anda akan diberitahu setelah permintaan disetujui"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Memulai login"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Kata Sandi Utama yang Terpapar"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nama benda"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Anda tidak dapat menghapus koleksi dengan izin hanya lihat: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organisasi dinonaktifkan"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Ekstra lebar"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Anda tidak dapat menghapus koleksi dengan izin hanya lihat: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/it/messages.json b/apps/browser/src/_locales/it/messages.json
index 0765cd0e419..9c00396b450 100644
--- a/apps/browser/src/_locales/it/messages.json
+++ b/apps/browser/src/_locales/it/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Una notifica è stata inviata al tuo dispositivo."
},
+ "notificationSentDevicePart1": {
+ "message": "Sblocca Bitwarden sul tuo dispositivo o su"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "app web"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Assicurarsi che la frase di impronta digitale corrisponda a quella sottostante prima dell'approvazione."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Una notifica è stata inviata al tuo dispositivo"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Sarai notificato una volta che la richiesta sarà approvata"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Accesso avviato"
},
+ "logInRequestSent": {
+ "message": "Richiesta inviata"
+ },
"exposedMasterPassword": {
"message": "Password principale violata"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nome elemento"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "L'organizzazione è disattivata"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Molto larga"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Non puoi rimuovere raccolte con i soli permessi di visualizzazione: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Aggiornare l'applicazione desktop"
},
diff --git a/apps/browser/src/_locales/ja/messages.json b/apps/browser/src/_locales/ja/messages.json
index fb927551b30..ca8141cb59f 100644
--- a/apps/browser/src/_locales/ja/messages.json
+++ b/apps/browser/src/_locales/ja/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "デバイスに通知を送信しました。"
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "お使いのデバイスに通知が送信されました"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "アカウントがロック解除されていることと、フィンガープリントフレーズが他の端末で一致していることを確認してください"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "リクエストが承認されると通知されます"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "ログイン開始"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "流出したマスターパスワード"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "アイテム名"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "組織は無効化されています"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "エクストラワイド"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "表示のみの権限が与えられているコレクションを削除することはできません: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/ka/messages.json b/apps/browser/src/_locales/ka/messages.json
index b1c5b7a5e58..5b81c015383 100644
--- a/apps/browser/src/_locales/ka/messages.json
+++ b/apps/browser/src/_locales/ka/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "ჩანაწერის სახელი"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/km/messages.json b/apps/browser/src/_locales/km/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/km/messages.json
+++ b/apps/browser/src/_locales/km/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/kn/messages.json b/apps/browser/src/_locales/kn/messages.json
index 69d05d03fe6..90359b0f73b 100644
--- a/apps/browser/src/_locales/kn/messages.json
+++ b/apps/browser/src/_locales/kn/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/ko/messages.json b/apps/browser/src/_locales/ko/messages.json
index 93cde61315c..896e22d57bb 100644
--- a/apps/browser/src/_locales/ko/messages.json
+++ b/apps/browser/src/_locales/ko/messages.json
@@ -1005,7 +1005,7 @@
"message": "보관함에 항목이 없을 경우 추가하라는 메시지를 표시합니다. 모든 로그인된 계정에 적용됩니다."
},
"showCardsInVaultViewV2": {
- "message": "Always show cards as Autofill suggestions on Vault view"
+ "message": "보관함 보기에서 언제나 카드 자동 완성 제안을 표시"
},
"showCardsCurrentTab": {
"message": "탭 페이지에 카드 표시"
@@ -1014,7 +1014,7 @@
"message": "간편한 자동완성을 위해 탭에 카드 항목들을 나열"
},
"showIdentitiesInVaultViewV2": {
- "message": "Always show identities as Autofill suggestions on Vault view"
+ "message": "보관함 보기에서 언제나 신원의 자동 완성 제안을 표시"
},
"showIdentitiesCurrentTab": {
"message": "탭 페이지에 신원들을 표시"
@@ -1125,10 +1125,10 @@
"message": "이 비밀번호는 이 파일을 파일 내보내거나, 가져오는데 사용됩니다."
},
"accountRestrictedOptionDescription": {
- "message": "계정의 사용자 이름과 마스터 비밀번호에서 파생된 계정 암호화 키를 사용하여 내보내기를 암호화하고, 현재 Bitwarden계정으만 가져오기를 제한합니다."
+ "message": "내보내기를 당신의 계정의 사용자이름과 마스터비밀번호로부터 파생된 계정 암호화 키를 사용하여 암호화하고, 현재의 Bitwarden 계정으로만 가져오도록 제한합니다."
},
"passwordProtectedOptionDescription": {
- "message": "파일 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 해독에 그 파일 비밀번호를 사용하여 가져오세요."
+ "message": "파일에 비밀번호를 설정하여 내보내기를 암호화하고, 어느 Bitwarden 계정으로든 그 비밀번호로 해독하여 가져오기 합니다."
},
"exportTypeHeading": {
"message": "내보내기 유형"
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "기기에 알림이 전송되었습니다."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "기기에 알림이 전송되었습니다."
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "반드시 계정이 잠금 해제되었고, 지문 구절이 다른 기기에서 일치하는지 확인해주세요."
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "요청이 승인되면 알림을 받게 됩니다"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "로그인 시작"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "노출된 마스터 비밀번호"
},
@@ -3994,7 +4003,7 @@
"message": "패스키 제거됨"
},
"autofillSuggestions": {
- "message": "Autofill suggestions"
+ "message": "자동 완성 제안"
},
"itemSuggestions": {
"message": "Suggested items"
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "항목 이름"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "조직이 비활성화되었습니다"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "매우 넓게"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "보기 권한만 있는 컬렉션은 제거할 수 없습니다: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/lt/messages.json b/apps/browser/src/_locales/lt/messages.json
index bb3e7e2357c..f64eb8b5189 100644
--- a/apps/browser/src/_locales/lt/messages.json
+++ b/apps/browser/src/_locales/lt/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Pradėtas prisijungimas"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Elemento pavadinimas"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Negalite pašalinti kolekcijų su Peržiūrėti tik leidimus: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/lv/messages.json b/apps/browser/src/_locales/lv/messages.json
index d256bed25a9..3306cdf5a8f 100644
--- a/apps/browser/src/_locales/lv/messages.json
+++ b/apps/browser/src/_locales/lv/messages.json
@@ -2062,7 +2062,7 @@
"message": "Lietotājvārdu veidotājs"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Izmantot šo e-pasta adresi"
},
"useThisPassword": {
"message": "Izmantot šo paroli"
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Uz ierīci ir nosūtīts paziņojums."
},
+ "notificationSentDevicePart1": {
+ "message": "Bitwarden jāatslēdz savā ierīcē vai"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "tīmekļa lietotnē"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Pirms apstiprināšanas jāpārliecinās, ka pirkstu nospieduma vārdkopa atbilst zemāk esošajai."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Uz ierīci tika nosūtīts paziņojums"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Lūgums pārliecināties, ka konts ir atslēgts un atpazīšanas vārdkopa ir tāda pati arī otrā ierīcē"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Tiks paziņots, tiklīdz pieprasījums būs apstiprināts"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Uzsākta pieteikšanās"
},
+ "logInRequestSent": {
+ "message": "Pieprasījums nosūtīts"
+ },
"exposedMasterPassword": {
"message": "Noplūdusi galvenā parole"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Vienuma nosaukums"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Apvienība ir atspējota"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Ļoti plats"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Nevar noņemt krājumus ar tiesībām \"Tikai skatīt\": $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Lūgums atjaunināt darbvirsmas lietotni"
},
diff --git a/apps/browser/src/_locales/ml/messages.json b/apps/browser/src/_locales/ml/messages.json
index b9d2858a5c9..75cf2bdd567 100644
--- a/apps/browser/src/_locales/ml/messages.json
+++ b/apps/browser/src/_locales/ml/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/mr/messages.json b/apps/browser/src/_locales/mr/messages.json
index 501f02e4e54..622cb1be639 100644
--- a/apps/browser/src/_locales/mr/messages.json
+++ b/apps/browser/src/_locales/mr/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/my/messages.json b/apps/browser/src/_locales/my/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/my/messages.json
+++ b/apps/browser/src/_locales/my/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/nb/messages.json b/apps/browser/src/_locales/nb/messages.json
index 101b314c9c2..ce50ff4f90a 100644
--- a/apps/browser/src/_locales/nb/messages.json
+++ b/apps/browser/src/_locales/nb/messages.json
@@ -7,7 +7,7 @@
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"
},
"extDesc": {
- "message": "At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information",
+ "message": "Hjemme, på jobben eller på farten sikrer Bitwarden enkelt alle dine passord, passnøkler og sensitiv informasjon",
"description": "Extension description, MUST be less than 112 characters (Safari restriction)"
},
"loginOrCreateNewAccount": {
@@ -141,7 +141,7 @@
"message": "Kopiér navn"
},
"copyCompany": {
- "message": "Copy company"
+ "message": "Kopiér firma"
},
"copySSN": {
"message": "Kopiér fødselsnummer"
@@ -281,13 +281,13 @@
"message": "Endre hovedpassordet"
},
"continueToWebApp": {
- "message": "Continue to web app?"
+ "message": "Vil du fortsette til nettappen?"
},
"continueToWebAppDesc": {
"message": "Explore more features of your Bitwarden account on the web app."
},
"continueToHelpCenter": {
- "message": "Continue to Help Center?"
+ "message": "Vil du fortsette til Hjelpesenteret?"
},
"continueToHelpCenterDesc": {
"message": "Learn more about how to use Bitwarden on the Help Center."
@@ -299,7 +299,7 @@
"message": "Help others find out if Bitwarden is right for them. Visit your browser's extension store and leave a rating now."
},
"changeMasterPasswordOnWebConfirmation": {
- "message": "You can change your master password on the Bitwarden web app."
+ "message": "Du kan endre hovedpassordet ditt i Bitwardens nettapp."
},
"fingerprintPhrase": {
"message": "Fingeravtrykksfrase",
@@ -322,16 +322,16 @@
"message": "Om"
},
"moreFromBitwarden": {
- "message": "More from Bitwarden"
+ "message": "Mer fra Bitwarden"
},
"continueToBitwardenDotCom": {
"message": "Vil du fortsette til bitwarden.com?"
},
"bitwardenForBusiness": {
- "message": "Bitwarden for Business"
+ "message": "Bitwarden for bedrifter"
},
"bitwardenAuthenticator": {
- "message": "Bitwarden Authenticator"
+ "message": "Bitwarden-autentiserer"
},
"continueToAuthenticatorPageDesc": {
"message": "Bitwarden Authenticator allows you to store authenticator keys and generate TOTP codes for 2-step verification flows. Learn more on the bitwarden.com website"
@@ -431,7 +431,7 @@
"message": "Generer automatisk sterke og unike passord for dine innlogginger."
},
"bitWebVaultApp": {
- "message": "Bitwarden web app"
+ "message": "Bitwardens nett-app"
},
"importItems": {
"message": "Importer elementer"
@@ -443,19 +443,19 @@
"message": "Generer et passord"
},
"generatePassphrase": {
- "message": "Generate passphrase"
+ "message": "Generér passordfrase"
},
"passwordGenerated": {
- "message": "Password generated"
+ "message": "Passord generert"
},
"passphraseGenerated": {
- "message": "Passphrase generated"
+ "message": "Passordfrase generert"
},
"usernameGenerated": {
- "message": "Username generated"
+ "message": "Brukernavn generert"
},
"emailGenerated": {
- "message": "Email generated"
+ "message": "E-postadresse generert"
},
"regeneratePassword": {
"message": "Omgenerer et passord"
@@ -567,7 +567,7 @@
"message": "Passord"
},
"totp": {
- "message": "Authenticator secret"
+ "message": "Autentiseringsnøkkel"
},
"passphrase": {
"message": "Passfrase"
@@ -633,7 +633,7 @@
"message": "Annet"
},
"unlockMethods": {
- "message": "Unlock options"
+ "message": "Opplåsingsalternativer"
},
"unlockMethodNeededToChangeTimeoutActionDesc": {
"message": "Set up an unlock method to change your vault timeout action."
@@ -642,10 +642,10 @@
"message": "Set up an unlock method in Settings"
},
"sessionTimeoutHeader": {
- "message": "Session timeout"
+ "message": "Tidsavbrudd for økten"
},
"vaultTimeoutHeader": {
- "message": "Vault timeout"
+ "message": "Tidsavbrudd for hvelvet"
},
"otherOptions": {
"message": "Andre valg"
@@ -663,16 +663,16 @@
"message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
},
"continueLoggingIn": {
- "message": "Continue logging in"
+ "message": "Fortsett innloggingen"
},
"yourVaultIsLocked": {
"message": "Hvelvet ditt er låst. Kontroller hovedpassordet ditt for å fortsette."
},
"yourVaultIsLockedV2": {
- "message": "Your vault is locked"
+ "message": "Hvelvet ditt er låst"
},
"yourAccountIsLocked": {
- "message": "Your account is locked"
+ "message": "Kontoen din er låst"
},
"or": {
"message": "eller"
@@ -797,10 +797,10 @@
"message": "Din nye konto har blitt opprettet! Du kan nå logge på."
},
"newAccountCreated2": {
- "message": "Your new account has been created!"
+ "message": "Den nye kontoen din er opprettet!"
},
"youHaveBeenLoggedIn": {
- "message": "You have been logged in!"
+ "message": "Du har blitt logget inn!"
},
"youSuccessfullyLoggedIn": {
"message": "Du har vellykket logget inn"
@@ -861,7 +861,7 @@
"message": "Logget av"
},
"loggedOutDesc": {
- "message": "You have been logged out of your account."
+ "message": "Du har blitt logget ut av kontoen din."
},
"loginExpired": {
"message": "Din innloggingsøkt har utløpt."
@@ -873,7 +873,7 @@
"message": "Logg inn på Bitwarden"
},
"restartRegistration": {
- "message": "Restart registration"
+ "message": "Start registreringen på nytt"
},
"expiredLink": {
"message": "Utløpt lenke"
@@ -909,7 +909,7 @@
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app."
},
"twoStepLoginConfirmationTitle": {
- "message": "Continue to web app?"
+ "message": "Vil du fortsette til nettappen?"
},
"editedFolder": {
"message": "Redigerte mappen"
@@ -1290,7 +1290,7 @@
"message": "Takk for at du støtter Bitwarden."
},
"premiumFeatures": {
- "message": "Upgrade to Premium and receive:"
+ "message": "Oppgrader til Premium og motta:"
},
"premiumPrice": {
"message": "Og alt det for %price%/år!",
@@ -1477,29 +1477,29 @@
"message": "Miljø-nettadressene har blitt lagret."
},
"showAutoFillMenuOnFormFields": {
- "message": "Show autofill menu on form fields",
+ "message": "Vis autoutfyll-menyen i tekstbokser",
"description": "Represents the message for allowing the user to enable the autofill overlay"
},
"autofillSuggestionsSectionTitle": {
"message": "Autoutfyllingsforslag"
},
"showInlineMenuLabel": {
- "message": "Show autofill suggestions on form fields"
+ "message": "Vis autoutfyll-forslag i tekstbokser"
},
"showInlineMenuIdentitiesLabel": {
- "message": "Display identities as suggestions"
+ "message": "Vis identiteter som forslag"
},
"showInlineMenuCardsLabel": {
- "message": "Display cards as suggestions"
+ "message": "Vis kort som forslag"
},
"showInlineMenuOnIconSelectionLabel": {
- "message": "Display suggestions when icon is selected"
+ "message": "Vis forslag når ikonet er valgt"
},
"showInlineMenuOnFormFieldsDescAlt": {
"message": "Applies to all logged in accounts."
},
"turnOffBrowserBuiltInPasswordManagerSettings": {
- "message": "Turn off your browser's built in password manager settings to avoid conflicts."
+ "message": "Skru av din nettlesers innebygde passordbehandler for å unngå konflikter."
},
"turnOffBrowserBuiltInPasswordManagerSettingsLink": {
"message": "Rediger nettleserinnstillingene."
@@ -1538,7 +1538,7 @@
"message": "Standard autofyll innstilling for innloggingselementer"
},
"defaultAutoFillOnPageLoadDesc": {
- "message": "Etter aktivering av auto-utfylling på sidelasser, kan du aktivere eller deaktivere funksjonen for individuelle innloggingselementer. Dette er standardinnstillingen for innloggingselementer som ikke er satt opp separat."
+ "message": "Du kan skru av auto-utfylling ved sideinnlastinger for individuelle innloggingsgjenstander fra gjenstandens «Redigér»-visning."
},
"itemAutoFillOnPageLoad": {
"message": "Auto-utfyll på sideinnlastning (hvis aktivert i Alternativer)"
@@ -1598,7 +1598,7 @@
"message": "Boolsk verdi"
},
"cfTypeCheckbox": {
- "message": "Checkbox"
+ "message": "Avkryssingsboks"
},
"cfTypeLinked": {
"message": "Tilkoblet",
@@ -1819,7 +1819,7 @@
"message": "Generatorhistorikk"
},
"clearGeneratorHistoryTitle": {
- "message": "Clear generator history"
+ "message": "Tøm generatorhistorikk"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
@@ -1831,7 +1831,7 @@
"message": "Samlinger"
},
"nCollections": {
- "message": "$COUNT$ collections",
+ "message": "$COUNT$ samlinger",
"placeholders": {
"count": {
"content": "$1",
@@ -1887,7 +1887,7 @@
"description": "Domain name. Ex. website.com"
},
"baseDomainOptionRecommended": {
- "message": "Base domain (recommended)",
+ "message": "Grunndomene (anbefalt)",
"description": "Domain name. Ex. website.com"
},
"domainName": {
@@ -1947,7 +1947,7 @@
"message": "Ingenting å vise"
},
"nothingGeneratedRecently": {
- "message": "You haven't generated anything recently"
+ "message": "Du har ikke generert noe i det siste"
},
"remove": {
"message": "Fjern"
@@ -2017,7 +2017,7 @@
"message": "Angi PIN-koden din for å låse opp Bitwarden. PIN-innstillingene tilbakestilles hvis du logger deg helt ut av programmet."
},
"setYourPinCode1": {
- "message": "Your PIN will be used to unlock Bitwarden instead of your master password. Your PIN will reset if you ever fully log out of Bitwarden."
+ "message": "PIN-koden din vil bli brukt til å låse opp Bitwarden i stedet for hovedpassordet ditt. PIN-koden din tilbakestilles hvis du noen gang logger deg helt ut av Bitwarden."
},
"pinRequired": {
"message": "PIN-kode er påkrevd."
@@ -2026,7 +2026,7 @@
"message": "Ugyldig PIN-kode."
},
"tooManyInvalidPinEntryAttemptsLoggingOut": {
- "message": "Too many invalid PIN entry attempts. Logging out."
+ "message": "For mange ugyldige PIN-kodeforsøk. Logger ut."
},
"unlockWithBiometrics": {
"message": "Lås opp med biometri"
@@ -2062,7 +2062,7 @@
"message": "Brukernavngenerator"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Bruk denne E-postadressen"
},
"useThisPassword": {
"message": "Bruk dette passordet"
@@ -2078,14 +2078,14 @@
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
},
"useGeneratorHelpTextPartTwo": {
- "message": "to create a strong unique password",
+ "message": "for å lage et sterkt og unikt passord",
"description": "This will be used as part of a larger sentence, broken up to include the generator icon. The full sentence will read 'Use the generator [GENERATOR_ICON] to create a strong unique password'"
},
"vaultTimeoutAction": {
"message": "Handling ved tidsavbrudd i hvelvet"
},
"vaultTimeoutAction1": {
- "message": "Timeout action"
+ "message": "Handling ved tidsavbrudd"
},
"lock": {
"message": "Lås",
@@ -2135,7 +2135,7 @@
"message": "Autoutfylt element"
},
"insecurePageWarning": {
- "message": "Warning: This is an unsecured HTTP page, and any information you submit can potentially be seen and changed by others. This Login was originally saved on a secure (HTTPS) page."
+ "message": "Advarsel: Dette er en usikret HTTP-side, og all informasjon du sender inn kan potensielt bli sett og endret av andre. Denne påloggingen ble opprinnelig lagret på et sikkert (HTTPS) nettsted."
},
"insecurePageWarningFillPrompt": {
"message": "Ønsker du likevel å fylle ut denne innloggingen?"
@@ -2306,7 +2306,7 @@
"message": "Please unlock this user in the desktop application and try again."
},
"biometricsNotAvailableTitle": {
- "message": "Biometric unlock unavailable"
+ "message": "Biometrisk opplåsing er utilgjengelig"
},
"biometricsNotAvailableDesc": {
"message": "Biometric unlock is currently unavailable. Please try again later."
@@ -2361,10 +2361,10 @@
"message": "Autofill is blocked for this website."
},
"autofillBlockedNoticeGuidance": {
- "message": "Change this in settings"
+ "message": "Endre dette i innstillingene"
},
"websiteItemLabel": {
- "message": "Website $number$ (URI)",
+ "message": "Nettsted $number$ (URİ)",
"placeholders": {
"number": {
"content": "$1",
@@ -2391,11 +2391,11 @@
"message": "Begrens visninger"
},
"limitSendViewsHint": {
- "message": "No one can view this Send after the limit is reached.",
+ "message": "Ingen kan se denne Send-en etter at grensen er nådd.",
"description": "Displayed under the limit views field on Send"
},
"limitSendViewsCount": {
- "message": "$ACCESSCOUNT$ views left",
+ "message": "$ACCESSCOUNT$ visninger igjen",
"description": "Displayed under the limit views field on Send",
"placeholders": {
"accessCount": {
@@ -2409,7 +2409,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDetails": {
- "message": "Send details",
+ "message": "Send-detaljer",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
@@ -2426,7 +2426,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"hideTextByDefault": {
- "message": "Hide text by default"
+ "message": "Skjul tekst som standard"
},
"expired": {
"message": "Utløpt"
@@ -2506,7 +2506,7 @@
"message": "Egendefinert"
},
"sendPasswordDescV3": {
- "message": "Add an optional password for recipients to access this Send.",
+ "message": "Legg til et valgfritt passord for at mottakerne skal få tilgang til denne Send-en.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createSend": {
@@ -2561,7 +2561,7 @@
}
},
"sendLinkCopied": {
- "message": "Send link copied",
+ "message": "Send-lenken ble kopiert",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editedSend": {
@@ -2569,7 +2569,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFilePopoutDialogText": {
- "message": "Pop out extension?",
+ "message": "Vil du sprette ut utvidelsen?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFilePopoutDialogDesc": {
@@ -2586,7 +2586,7 @@
"message": "For å velge en fil med Safari, popp ut i et nytt vindu ved å klikke på dette banneret."
},
"popOut": {
- "message": "Pop out"
+ "message": "Sprett ut"
},
"sendFileCalloutHeader": {
"message": "Før du starter"
@@ -2607,7 +2607,7 @@
"message": "Det oppstod en feil ved lagring av slettingen og utløpsdatoene."
},
"hideYourEmail": {
- "message": "Hide your email address from viewers."
+ "message": "Skjul E-postadressen din fra seere."
},
"passwordPrompt": {
"message": "Forespørsel om hovedpassord på nytt"
@@ -2833,14 +2833,14 @@
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"contactCSToAvoidDataLossPart2": {
- "message": "to avoid additional data loss.",
+ "message": "for å unngå ytterligere datatap.",
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"generateUsername": {
"message": "Generer brukernavn"
},
"generateEmail": {
- "message": "Generate email"
+ "message": "Generér E-post"
},
"spinboxBoundariesHint": {
"message": "Verdien må være mellom $MIN$ og $MAX$.",
@@ -2857,7 +2857,7 @@
}
},
"passwordLengthRecommendationHint": {
- "message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
+ "message": " Bruk minst $RECOMMENDED$ tegn for å generere et sterkt passord.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2912,7 +2912,7 @@
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
- "message": "Choose a domain that is supported by the selected service",
+ "message": "Velg et domene som støttes av den valgte tjenesten",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@@ -2944,7 +2944,7 @@
}
},
"forwaderInvalidToken": {
- "message": "Invalid $SERVICENAME$ API token",
+ "message": "Ugyldig $SERVICENAME$-API-sjetong",
"description": "Displayed when the user's API token is empty or rejected by the forwarding service.",
"placeholders": {
"servicename": {
@@ -2954,7 +2954,7 @@
}
},
"forwaderInvalidTokenWithMessage": {
- "message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$",
+ "message": "Ugyldig $SERVICENAME$-API-sjetong: $ERRORMESSAGE$",
"description": "Displayed when the user's API token is rejected by the forwarding service with an error message.",
"placeholders": {
"servicename": {
@@ -2998,7 +2998,7 @@
}
},
"forwarderUnknownError": {
- "message": "Unknown $SERVICENAME$ error occurred.",
+ "message": "Ukjent $SERVICENAME$-feil oppstod.",
"description": "Displayed when the forwarding service failed due to an unknown error.",
"placeholders": {
"servicename": {
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Et varsel er sendt til enheten din."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "nett-app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Et varsel ble sendt til enheten din"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3136,7 +3142,10 @@
"message": "Trenger du et annet alternativ?"
},
"loginInitiated": {
- "message": "Login initiated"
+ "message": "Innlogging igangsatt"
+ },
+ "logInRequestSent": {
+ "message": "Forespørsel sendt"
},
"exposedMasterPassword": {
"message": "Eksponert hovedpassord"
@@ -3196,10 +3205,10 @@
"message": "Autofill shortcut"
},
"autofillKeyboardShortcutUpdateLabel": {
- "message": "Change shortcut"
+ "message": "Endre snarvei"
},
"autofillKeyboardManagerShortcutsLabel": {
- "message": "Manage shortcuts"
+ "message": "Behandle snarveier"
},
"autofillShortcut": {
"message": "Auto-utfyll tastatursnarvei"
@@ -3208,7 +3217,7 @@
"message": "The autofill login shortcut is not set. Change this in the browser's settings."
},
"autofillLoginShortcutText": {
- "message": "The autofill login shortcut is $COMMAND$. Manage all shortcuts in the browser's settings.",
+ "message": "Autoutfyll-snarveien for pålogging er $COMMAND$. Håndter alle snarveiene i nettleserens innstillinger.",
"placeholders": {
"command": {
"content": "$1",
@@ -3262,16 +3271,16 @@
"message": "Oppretter en konto på"
},
"checkYourEmail": {
- "message": "Check your email"
+ "message": "Sjekk E-postinnboksen din"
},
"followTheLinkInTheEmailSentTo": {
- "message": "Follow the link in the email sent to"
+ "message": "Følg lenken i E-postadressen som ble sendt til"
},
"andContinueCreatingYourAccount": {
"message": "and continue creating your account."
},
"noEmail": {
- "message": "No email?"
+ "message": "Ingen E-post?"
},
"goBack": {
"message": "Gå tilbake"
@@ -3320,11 +3329,11 @@
"message": "Enheten er betrodd"
},
"sendsNoItemsTitle": {
- "message": "No active Sends",
+ "message": "Ingen aktive Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendsNoItemsMessage": {
- "message": "Use Send to securely share encrypted information with anyone.",
+ "message": "Bruk Send til å dele kryptert informasjon med noen.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"inputRequired": {
@@ -3401,10 +3410,10 @@
}
},
"singleFieldNeedsAttention": {
- "message": "1 field needs your attention."
+ "message": "1 felt trenger din oppmerksomhet."
},
"multipleFieldsNeedAttention": {
- "message": "$COUNT$ fields need your attention.",
+ "message": "$COUNT$ felter trenger din oppmerksomhet.",
"placeholders": {
"count": {
"content": "$1",
@@ -3440,7 +3449,7 @@
"message": "Undermeny"
},
"toggleCollapse": {
- "message": "Toggle collapse",
+ "message": "Utvid eller klapp sammen",
"description": "Toggling an expand/collapse state."
},
"aliasDomain": {
@@ -3646,7 +3655,7 @@
"message": "Popout extension"
},
"launchDuo": {
- "message": "Launch Duo"
+ "message": "Start Duo"
},
"importFormatError": {
"message": "Data is not formatted correctly. Please check your import file and try again."
@@ -3720,13 +3729,13 @@
"message": "Bekreft filpassord"
},
"exportSuccess": {
- "message": "Vault data exported"
+ "message": "Hvelvdataen ble eksportert"
},
"typePasskey": {
"message": "Passnøkkel"
},
"accessing": {
- "message": "Accessing"
+ "message": "Logger inn på"
},
"loggedInExclamation": {
"message": "Innlogget!"
@@ -3756,7 +3765,7 @@
"message": "No matching logins for this site"
},
"searchSavePasskeyNewLogin": {
- "message": "Search or save passkey as new login"
+ "message": "Søk eller lagre passnøkkelen som en ny innlogging"
},
"confirm": {
"message": "Bekreft"
@@ -3774,7 +3783,7 @@
"message": "Choose a passkey to log in with"
},
"passkeyItem": {
- "message": "Passkey Item"
+ "message": "Passkode-gjenstand"
},
"overwritePasskey": {
"message": "Overwrite passkey?"
@@ -3921,7 +3930,7 @@
"description": "Title for dialog which asks if the user wants to proceed to a relevant browser settings page"
},
"confirmContinueToHelpCenter": {
- "message": "Continue to Help Center?",
+ "message": "Vil du fortsette til Hjelpesenteret?",
"description": "Title for dialog which asks if the user wants to proceed to a relevant Help Center page"
},
"confirmContinueToHelpCenterPasswordManagementContent": {
@@ -3941,7 +3950,7 @@
"description": "Body content for dialog which asks if the user wants to proceed to the browser's keyboard shortcut settings page"
},
"overrideDefaultBrowserAutofillTitle": {
- "message": "Make Bitwarden your default password manager?",
+ "message": "Vil du sette Bitwarden som din standard passordbehandler?",
"description": "Dialog title facilitating the ability to override a chrome browser's default autofill behavior"
},
"overrideDefaultBrowserAutofillDescription": {
@@ -3994,10 +4003,10 @@
"message": "Passkey removed"
},
"autofillSuggestions": {
- "message": "Autofill suggestions"
+ "message": "Autoutfyllingsforslag"
},
"itemSuggestions": {
- "message": "Suggested items"
+ "message": "Foreslåtte gjenstander"
},
"autofillSuggestionsTip": {
"message": "Save a login item for this site to autofill"
@@ -4012,7 +4021,7 @@
"message": "Clear filters or try another search term"
},
"copyInfoTitle": {
- "message": "Copy info - $ITEMNAME$",
+ "message": "Kopiér info - $ITEMNAME$",
"description": "Title for a button that opens a menu with options to copy information from an item.",
"placeholders": {
"itemname": {
@@ -4022,7 +4031,7 @@
}
},
"copyNoteTitle": {
- "message": "Copy Note - $ITEMNAME$",
+ "message": "Kopiér notat - $ITEMNAME$",
"description": "Title for a button copies a note to the clipboard.",
"placeholders": {
"itemname": {
@@ -4072,7 +4081,7 @@
}
},
"noValuesToCopy": {
- "message": "No values to copy"
+ "message": "Ingen verdier å kopiere"
},
"assignToCollections": {
"message": "Legg til i samlinger"
@@ -4081,10 +4090,10 @@
"message": "Copy email"
},
"copyPhone": {
- "message": "Copy phone"
+ "message": "Kopiér telefonnummer"
},
"copyAddress": {
- "message": "Copy address"
+ "message": "Kopiér adresse"
},
"adminConsole": {
"message": "Admin Console"
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Gjenstandens navn"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Organization is deactivated"
},
@@ -4178,13 +4178,13 @@
"message": "Nyligst redigert"
},
"ownerYou": {
- "message": "Owner: You"
+ "message": "Eier: Du"
},
"linked": {
"message": "Tilknyttet"
},
"copySuccessful": {
- "message": "Copy Successful"
+ "message": "Kopiering lyktes"
},
"upload": {
"message": "Last opp"
@@ -4196,7 +4196,7 @@
"message": "Maksimal filstørrelse er 500 MB"
},
"deleteAttachmentName": {
- "message": "Delete attachment $NAME$",
+ "message": "Slett $NAME$-vedlegget",
"placeholders": {
"name": {
"content": "$1",
@@ -4272,10 +4272,10 @@
"message": "Autoutfyllings-innstillinger"
},
"websiteUri": {
- "message": "Website (URI)"
+ "message": "Nettsted (URİ)"
},
"websiteUriCount": {
- "message": "Website (URI) $COUNT$",
+ "message": "Nettsted (URİ) $COUNT$",
"description": "Label for an input field that contains a website URI. The input field is part of a list of fields, and the count indicates the position of the field in the list.",
"placeholders": {
"count": {
@@ -4403,7 +4403,7 @@
"message": "Feltetikett"
},
"textHelpText": {
- "message": "Use text fields for data like security questions"
+ "message": "Bruk tekstfelter for data som sikkerhetsspørsmål"
},
"hiddenHelpText": {
"message": "Use hidden fields for sensitive data like a password"
@@ -4421,7 +4421,7 @@
"message": "Rediger felt"
},
"editFieldLabel": {
- "message": "Edit $LABEL$",
+ "message": "Rediger $LABEL$",
"placeholders": {
"label": {
"content": "$1",
@@ -4474,7 +4474,7 @@
}
},
"selectCollectionsToAssign": {
- "message": "Select collections to assign"
+ "message": "Velg samlinger å tilordne"
},
"personalItemTransferWarningSingular": {
"message": "1 item will be permanently transferred to the selected organization. You will no longer own this item."
@@ -4514,10 +4514,10 @@
"message": "Successfully assigned collections"
},
"nothingSelected": {
- "message": "You have not selected anything."
+ "message": "Du har ikke valgt noe."
},
"movedItemsToOrg": {
- "message": "Selected items moved to $ORGNAME$",
+ "message": "De valgte gjenstandene ble flyttet til $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4526,7 +4526,7 @@
}
},
"itemsMovedToOrg": {
- "message": "Items moved to $ORGNAME$",
+ "message": "Gjenstandene ble flyttet til $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4535,7 +4535,7 @@
}
},
"itemMovedToOrg": {
- "message": "Item moved to $ORGNAME$",
+ "message": "Gjenstanden ble flyttet til $ORGNAME$",
"placeholders": {
"orgname": {
"content": "$1",
@@ -4576,13 +4576,13 @@
"message": "Text Sends"
},
"accountActions": {
- "message": "Account actions"
+ "message": "Kontohandlinger"
},
"showNumberOfAutofillSuggestions": {
"message": "Show number of login autofill suggestions on extension icon"
},
"showQuickCopyActions": {
- "message": "Show quick copy actions on Vault"
+ "message": "Vis hurtigkopieringshandlinger i hvelvet"
},
"systemDefault": {
"message": "Systemforvalg"
@@ -4618,7 +4618,7 @@
"message": "Prøv igjen"
},
"vaultCustomTimeoutMinimum": {
- "message": "Minimum custom timeout is 1 minute."
+ "message": "Minste egendefinerte tidsavbrudd er 1 minutt."
},
"additionalContentAvailable": {
"message": "Ytterligere innhold er tilgjengelig"
@@ -4639,10 +4639,10 @@
"message": "Ingen gjenstander i papirkurven"
},
"noItemsInTrashDesc": {
- "message": "Items you delete will appear here and be permanently deleted after 30 days"
+ "message": "Gjenstander du sletter, vises her og slettes permanent etter 30 dager"
},
"trashWarning": {
- "message": "Items that have been in trash more than 30 days will automatically be deleted"
+ "message": "Gjenstander som har ligget i papirkurven i mer enn 30 dager, blir automatisk slettet"
},
"restore": {
"message": "Gjenopprett"
@@ -4657,7 +4657,7 @@
"message": "Biometric unlock is unavailable because PIN or password unlock is required first."
},
"biometricsStatusHelptextHardwareUnavailable": {
- "message": "Biometric unlock is currently unavailable."
+ "message": "Biometrisk opplåsing er utilgjengelig for øyeblikket."
},
"biometricsStatusHelptextAutoSetupNeeded": {
"message": "Biometric unlock is unavailable due to misconfigured system files."
@@ -4684,11 +4684,11 @@
"message": "Autentiserer"
},
"fillGeneratedPassword": {
- "message": "Fill generated password",
+ "message": "Fyll inn generert passord",
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
- "message": "Password regenerated",
+ "message": "Passord ble generert på nytt",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
@@ -4855,7 +4855,7 @@
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
},
"remindMeLater": {
- "message": "Remind me later"
+ "message": "Minn meg på det senere"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
@@ -4873,7 +4873,7 @@
"message": "Yes, I can reliably access my email"
},
"turnOnTwoStepLogin": {
- "message": "Turn on two-step login"
+ "message": "Slå på 2-trinnsinnlogging"
},
"changeAcctEmail": {
"message": "Endre kontoens E-postadresse"
@@ -4887,10 +4887,19 @@
"extraWide": {
"message": "Ekstra bred"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
- "message": "Please update your desktop application"
+ "message": "Vennligst oppdater skrivebordsprogrammet ditt"
},
"updateDesktopAppOrDisableFingerprintDialogMessage": {
- "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings."
+ "message": "For å bruke biometrisk opplåsing, må du oppdatere skrivebordsprogrammet eller skru av fingeravtrykksopplåsing i skrivebordsinnstillingene."
}
}
diff --git a/apps/browser/src/_locales/ne/messages.json b/apps/browser/src/_locales/ne/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/ne/messages.json
+++ b/apps/browser/src/_locales/ne/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/nl/messages.json b/apps/browser/src/_locales/nl/messages.json
index 46dc3e1166d..076c35732c0 100644
--- a/apps/browser/src/_locales/nl/messages.json
+++ b/apps/browser/src/_locales/nl/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Er is een melding naar je apparaat verzonden."
},
+ "notificationSentDevicePart1": {
+ "message": "Ontgrendel Bitwarden op je apparaat of op de"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "webapp"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Zorg ervoor dat de Vingerafdrukzin overeenkomt met de onderstaande voor je deze goedkeurt."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Er is een melding naar je apparaat verzonden"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Zorg ervoor dat je kluis is ontgrendeld en de vingerafdrukzin hetzelfde is op het andere apparaat"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Je krijgt een melding zodra de aanvraag is goedgekeurd"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Inloggen gestart"
},
+ "logInRequestSent": {
+ "message": "Verzoek verzonden"
+ },
"exposedMasterPassword": {
"message": "Gelekt hoofdwachtwoord"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra breed"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Je kunt verzamelingen niet verwijderen met alleen rechten voor weergeven: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Werk je desktopapplicatie bij"
},
diff --git a/apps/browser/src/_locales/nn/messages.json b/apps/browser/src/_locales/nn/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/nn/messages.json
+++ b/apps/browser/src/_locales/nn/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/or/messages.json b/apps/browser/src/_locales/or/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/or/messages.json
+++ b/apps/browser/src/_locales/or/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/pl/messages.json b/apps/browser/src/_locales/pl/messages.json
index 8d8f38603c5..232ec85c80a 100644
--- a/apps/browser/src/_locales/pl/messages.json
+++ b/apps/browser/src/_locales/pl/messages.json
@@ -23,7 +23,7 @@
"message": "Nowy użytkownik Bitwarden?"
},
"logInWithPasskey": {
- "message": "Zaloguj się używając passkey"
+ "message": "Zaloguj się używając klucza dostępu"
},
"useSingleSignOn": {
"message": "Użyj jednokrotnego logowania"
@@ -120,7 +120,7 @@
"message": "Kopiuj hasło"
},
"copyPassphrase": {
- "message": "Kopiuj frazę bezpieczeństwa"
+ "message": "Skopiuj hasło wyrazowe"
},
"copyNote": {
"message": "Kopiuj notatkę"
@@ -147,7 +147,7 @@
"message": "Kopiuj numer PESEL"
},
"copyPassportNumber": {
- "message": "Kopiuj numer paszportu"
+ "message": "Skopiuj numer paszportu"
},
"copyLicenseNumber": {
"message": "Kopiuj numer licencji"
@@ -443,19 +443,19 @@
"message": "Wygeneruj hasło"
},
"generatePassphrase": {
- "message": "Wygenruj frazę zabezpieczającą"
+ "message": "Wygeneruj hasło wyrazowe"
},
"passwordGenerated": {
- "message": "Password generated"
+ "message": "Hasło zostało wygenerowane"
},
"passphraseGenerated": {
- "message": "Passphrase generated"
+ "message": "Hasło wyrazowe zostało wygenerowane"
},
"usernameGenerated": {
- "message": "Username generated"
+ "message": "Nazwa użytkownika została wygenerowana"
},
"emailGenerated": {
- "message": "Email generated"
+ "message": "E-mail został wygenerowany"
},
"regeneratePassword": {
"message": "Wygeneruj ponownie hasło"
@@ -660,10 +660,10 @@
"message": "Zweryfikuj tożsamość"
},
"weDontRecognizeThisDevice": {
- "message": "We don't recognize this device. Enter the code sent to your email to verify your identity."
+ "message": "Nie rozpoznajemy tego urządzenia. Wpisz kod wysłany na Twój e-mail, aby zweryfikować tożsamość."
},
"continueLoggingIn": {
- "message": "Continue logging in"
+ "message": "Kontynuuj logowanie"
},
"yourVaultIsLocked": {
"message": "Sejf jest zablokowany. Zweryfikuj swoją tożsamość, aby kontynuować."
@@ -806,7 +806,7 @@
"message": "Zalogowałeś się pomyślnie"
},
"youMayCloseThisWindow": {
- "message": "Możesz zamknąć to okno."
+ "message": "Możesz zamknąć to okno"
},
"masterPassSent": {
"message": "Wysłaliśmy Tobie wiadomość e-mail z podpowiedzią do hasła głównego."
@@ -1005,7 +1005,7 @@
"message": "Poproś o dodanie elementu, jeśli nie zostanie znaleziony w Twoim sejfie. Dotyczy wszystkich zalogowanych kont."
},
"showCardsInVaultViewV2": {
- "message": "Always show cards as Autofill suggestions on Vault view"
+ "message": "Zawsze pokazuj karty jako sugestie autouzupełniania w widoku sejfu"
},
"showCardsCurrentTab": {
"message": "Pokaż karty na stronie głównej"
@@ -1014,7 +1014,7 @@
"message": "Pokaż elementy karty na stronie głównej, aby ułatwić autouzupełnianie."
},
"showIdentitiesInVaultViewV2": {
- "message": "Always show identities as Autofill suggestions on Vault view"
+ "message": "Zawsze pokazuj tożsamości jako sugestie autouzupełniania w widoku sejfu"
},
"showIdentitiesCurrentTab": {
"message": "Pokaż tożsamości na stronie głównej"
@@ -1049,10 +1049,10 @@
"message": "Poproś o aktualizację hasła, gdy zmiana zostanie wykryta na stronie. Dotyczy wszystkich zalogowanych kont."
},
"enableUsePasskeys": {
- "message": "Pytaj o zapisywanie i używanie passkey"
+ "message": "Pytaj o zapisywanie i używanie kluczy dostępu"
},
"usePasskeysDesc": {
- "message": "Pytaj o zapisywanie nowych passkey albo danych logowania z passkey w Twoim sejfie. Dotyczy wszystkich zalogowanych kont."
+ "message": "Pytaj o zapisywanie nowych kluczy dostępu albo danych logowania z kluczy w Twoim sejfie. Dotyczy wszystkich zalogowanych kont."
},
"notificationChangeDesc": {
"message": "Czy chcesz zaktualizować to hasło w Bitwarden?"
@@ -1073,7 +1073,7 @@
"message": "Pokaż opcje menu kontekstowego"
},
"contextMenuItemDesc": {
- "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny. "
+ "message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny."
},
"contextMenuItemDescAlt": {
"message": "Użyj drugiego kliknięcia, aby uzyskać dostęp do generowania haseł i pasujących danych logowania do witryny. Dotyczy wszystkich zalogowanych kont."
@@ -1493,7 +1493,7 @@
"message": "Pokazuj karty jako sugestie"
},
"showInlineMenuOnIconSelectionLabel": {
- "message": "Wyświetlaj sugestie kiedy ikona jest zaznaczona"
+ "message": "Wyświetlaj sugestie, kiedy ikona jest zaznaczona"
},
"showInlineMenuOnFormFieldsDescAlt": {
"message": "Dotyczy wszystkich zalogowanych kont."
@@ -1538,7 +1538,7 @@
"message": "Domyślne ustawienie autouzupełniania"
},
"defaultAutoFillOnPageLoadDesc": {
- "message": "Po włączeniu autouzupełnianiu po załadowaniu strony, możesz włączyć lub wyłączyć tę funkcję dla poszczególnych wpisów."
+ "message": "Po włączeniu autouzupełnianiu po załadowaniu strony możesz włączyć lub wyłączyć tę funkcję dla poszczególnych wpisów."
},
"itemAutoFillOnPageLoad": {
"message": "Automatycznie uzupełniaj po załadowaniu strony (jeśli włączono w opcjach)"
@@ -2062,7 +2062,7 @@
"message": "Generator nazw użytkownika"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Użyj tego adresu e-mail"
},
"useThisPassword": {
"message": "Użyj tego hasła"
@@ -2132,7 +2132,7 @@
"message": "URI został zapisany i automatycznie uzupełniony"
},
"autoFillSuccess": {
- "message": "Element został automatycznie uzupełniony"
+ "message": "Element został automatycznie uzupełniony "
},
"insecurePageWarning": {
"message": "Ostrzeżenie: Jest to niezabezpieczona strona HTTP i wszelkie przekazane informacje mogą być potencjalnie widoczne i zmienione przez innych. Ten login został pierwotnie zapisany na stronie bezpiecznej (HTTPS)."
@@ -2141,7 +2141,7 @@
"message": "Nadal chcesz uzupełnić ten login?"
},
"autofillIframeWarning": {
- "message": "Formularz jest hostowany przez inną domenę niż zapisany adres URI dla tego loginu. Wybierz OK, aby i tak automatycznie wypełnić lub anuluj aby zatrzymać."
+ "message": "Formularz jest hostowany przez inną domenę niż zapisany adres URI dla tego loginu. Wybierz OK, aby i tak automatycznie wypełnić lub anuluj, aby zatrzymać."
},
"autofillIframeWarningTip": {
"message": "Aby zapobiec temu ostrzeżeniu w przyszłości, zapisz ten URI, $HOSTNAME$, dla tej witryny.",
@@ -2285,7 +2285,7 @@
"message": "Klucz biometryczny jest niepoprawny"
},
"nativeMessagingWrongUserKeyDesc": {
- "message": "Odblokowanie biometryczne nie powiodło się. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie."
+ "message": "Odblokowanie biometryczne się nie powiodło. Sekretny klucz biometryczny nie odblokował sejfu. Spróbuj skonfigurować biometrię ponownie."
},
"biometricsNotEnabledTitle": {
"message": "Dane biometryczne są wyłączone"
@@ -2355,7 +2355,7 @@
"message": "Aplikacja Bitwarden nie będzie proponować zapisywania danych logowania dla tych domen dla wszystkich zalogowanych kont. Musisz odświeżyć stronę, aby zastosowywać zmiany."
},
"blockedDomainsDesc": {
- "message": "Autofill and other related features will not be offered for these websites. You must refresh the page for changes to take effect."
+ "message": "Autouzupełnianie i inne powiązane funkcje nie będą oferowane dla tych stron. Aby zmiany zaczęły obowiązywać, musisz odświeżyć stronę."
},
"autofillBlockedNoticeV2": {
"message": "Autouzupełnianie jest zablokowane dla tej witryny."
@@ -2573,7 +2573,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendFilePopoutDialogDesc": {
- "message": "To create a file Send, you need to pop out the extension to a new window.",
+ "message": "Aby utworzyć plik Send, musisz wysunąć rozszerzenie do nowego okna.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLinuxChromiumFileWarning": {
@@ -2829,11 +2829,11 @@
"message": "Bitwarden nie mógł odszyfrować elementów sejfu wymienionych poniżej."
},
"contactCSToAvoidDataLossPart1": {
- "message": "Contact customer success",
+ "message": "Skontaktuj się z działem obsługi klienta,",
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"contactCSToAvoidDataLossPart2": {
- "message": "to avoid additional data loss.",
+ "message": "aby uniknąć dalszej utraty danych.",
"description": "This is part of a larger sentence. The full sentence will read 'Contact customer success to avoid additional data loss.'"
},
"generateUsername": {
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Powiadomienie zostało wysłane na urządzenie."
},
+ "notificationSentDevicePart1": {
+ "message": "Odblokuj Bitwarden na swoim urządzeniu lub w"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "aplikacji internetowej"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Upewnij się, że fraza odcisku palca zgadza się z tą poniżej, zanim zatwierdzisz."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Powiadomienie zostało wysłane na twoje urządzenie"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Upewnij się, że Twoje konto jest odblokowane, a unikalny identyfikator konta pasuje do drugiego urządzenia"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Zostaniesz powiadomiony po zatwierdzeniu prośby"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Logowanie rozpoczęte"
},
+ "logInRequestSent": {
+ "message": "Żądanie wysłane"
+ },
"exposedMasterPassword": {
"message": "Ujawnione hasło główne"
},
@@ -3169,7 +3178,7 @@
}
},
"autofillPageLoadPolicyActivated": {
- "message": "Twoja organizacji włączyła autouzupełnianie podczas wczytywania strony."
+ "message": "Twoja organizacja włączyła autouzupełnianie podczas wczytywania strony."
},
"howToAutofill": {
"message": "Jak autouzupełniać"
@@ -3244,7 +3253,7 @@
"message": "Zapamiętaj to urządzenie"
},
"uncheckIfPublicDevice": {
- "message": "Odznacz jeśli używasz publicznego urządzenia"
+ "message": "Odznacz, jeśli używasz publicznego urządzenia"
},
"approveFromYourOtherDevice": {
"message": "Zatwierdź z innego twojego urządzenia"
@@ -3447,7 +3456,7 @@
"message": "Domena aliasu"
},
"passwordRepromptDisabledAutofillOnPageLoad": {
- "message": "Elementy z pytaniem o hasło głównege nie mogą być automatycznie wypełniane przy wczytywaniu strony. Automatyczne wypełnianie po wczytywania strony zostało wyłączone.",
+ "message": "Elementy z pytaniem o hasło główne nie mogą być autouzupełniane przy wczytywaniu strony. Autouzupełnianie podczas wczytywania strony zostało wyłączone.",
"description": "Toast message for describing that master password re-prompt cannot be autofilled on page load."
},
"autofillOnPageLoadSetToDefault": {
@@ -3493,7 +3502,7 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"totpCodeAria": {
- "message": "Time-based One-Time Password Verification Code",
+ "message": "Kod weryfikacyjny jednorazowego hasła oparty na czasie",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
@@ -3723,7 +3732,7 @@
"message": "Dane sejfu zostały wyeksportowane"
},
"typePasskey": {
- "message": "Passkey"
+ "message": "Klucz dostępu"
},
"accessing": {
"message": "Uzyskiwanie dostępu"
@@ -3732,22 +3741,22 @@
"message": "Zalogowano!"
},
"passkeyNotCopied": {
- "message": "Passkey nie zostanie skopiowany"
+ "message": "Klucz dostępu nie zostanie skopiowany"
},
"passkeyNotCopiedAlert": {
- "message": "Passkey nie zostanie skopiowane do sklonowanego elementu. Czy chcesz kontynuować klonowanie tego elementu?"
+ "message": "Klucz dostępu nie zostanie skopiowany do sklonowanego elementu. Czy chcesz kontynuować klonowanie tego elementu?"
},
"passkeyFeatureIsNotImplementedForAccountsWithoutMasterPassword": {
"message": "Weryfikacja jest wymagana przez stronę inicjującą. Ta funkcja nie jest jeszcze zaimplementowana dla kont bez hasła głównego."
},
"logInWithPasskeyQuestion": {
- "message": "Zaloguj się za pomocą passkey?"
+ "message": "Zalogować za pomocą klucza dostępu?"
},
"passkeyAlreadyExists": {
- "message": "Passkey już istnieje dla tej aplikacji."
+ "message": "Klucz dostępu już istnieje dla tej aplikacji."
},
"noPasskeysFoundForThisApplication": {
- "message": "Nie znaleziono passkey'a dla tej aplikacji."
+ "message": "Nie znaleziono klucza dostępu dla tej aplikacji."
},
"noMatchingPasskeyLogin": {
"message": "Nie masz pasujących danych logowania do tej witryny."
@@ -3756,37 +3765,37 @@
"message": "Brak pasujących loginów dla tej witryny"
},
"searchSavePasskeyNewLogin": {
- "message": "Wyszukaj alb zapisz passkey jako nowy login"
+ "message": "Wyszukaj albo zapisz klucz dostępu jako nowy login"
},
"confirm": {
"message": "Potwierdź"
},
"savePasskey": {
- "message": "Zapisz passkey"
+ "message": "Zapisz klucz dostępu"
},
"savePasskeyNewLogin": {
- "message": "Zapisz passkey jako nowe dane logowania"
+ "message": "Zapisz klucz dostępu jako nowe dane logowania"
},
"chooseCipherForPasskeySave": {
- "message": "Wybierz dane logowania do których przypisać passkey"
+ "message": "Wybierz dane logowania, do których przypisać klucz dostępu"
},
"chooseCipherForPasskeyAuth": {
- "message": "Wybierz passkey żeby się zalogować"
+ "message": "Wybierz klucz dostępu, żeby się zalogować"
},
"passkeyItem": {
- "message": "Element Passkey"
+ "message": "Element klucza dostępu"
},
"overwritePasskey": {
- "message": "Zastąpić passkey?"
+ "message": "Zastąpić klucz dostępu?"
},
"overwritePasskeyAlert": {
- "message": "Ten element zawiera już passkey. Czy na pewno chcesz nadpisać bieżący passkey?"
+ "message": "Ten element zawiera już klucz dostępu. Czy na pewno chcesz nadpisać bieżący klucza dostępu?"
},
"featureNotSupported": {
"message": "Funkcja nie jest jeszcze obsługiwana"
},
"yourPasskeyIsLocked": {
- "message": "Wymagane uwierzytelnienie aby używać passkey. Sprawdź swoją tożsamość, aby kontynuować."
+ "message": "Wymagane uwierzytelnienie, aby używać klucza dostępu. Sprawdź swoją tożsamość, aby kontynuować."
},
"multifactorAuthenticationCancelled": {
"message": "Uwierzytelnianie wieloskładnikowe zostało anulowane"
@@ -3988,10 +3997,10 @@
"message": "Sukces"
},
"removePasskey": {
- "message": "Usuń passkey"
+ "message": "Usuń klucz dostępu"
},
"passkeyRemoved": {
- "message": "Passkey został usunięty"
+ "message": "Klucz dostępu został usunięty"
},
"autofillSuggestions": {
"message": "Sugestie autouzupełniania"
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4358,7 +4358,7 @@
"message": "Dane"
},
"passkeys": {
- "message": "Passkeys",
+ "message": "Klucze dostępu",
"description": "A section header for a list of passkeys."
},
"passwords": {
@@ -4366,7 +4366,7 @@
"description": "A section header for a list of passwords."
},
"logInWithPasskeyAriaLabel": {
- "message": "Zaloguj się za pomocą passkey",
+ "message": "Zaloguj się za pomocą klucza dostępu",
"description": "ARIA label for the inline menu button that logs in with a passkey."
},
"assign": {
@@ -4448,7 +4448,7 @@
}
},
"reorderToggleButton": {
- "message": "Zmień kolejność $LABEL$. Użyj klawiszy że strzałkami aby przenieść element w górę lub w dół.",
+ "message": "Zmień kolejność $LABEL$. Użyj klawiszy ze strzałkami, aby przenieść element w górę lub w dół.",
"placeholders": {
"label": {
"content": "$1",
@@ -4564,16 +4564,16 @@
"message": "Lokalizacja elementu"
},
"fileSend": {
- "message": "File Send"
+ "message": "Wysyłka pliku"
},
"fileSends": {
- "message": "File Sends"
+ "message": "Wysyłki plików"
},
"textSend": {
- "message": "Text Send"
+ "message": "Wysyłka tekstu"
},
"textSends": {
- "message": "Text Sends"
+ "message": "Wysyłki tekstów"
},
"accountActions": {
"message": "Akcje konta"
@@ -4704,7 +4704,7 @@
"description": "Represents the ~ key in screen reader content as a readable word"
},
"backtickCharacterDescriptor": {
- "message": "Backtick",
+ "message": "Grawis",
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
@@ -4728,7 +4728,7 @@
"description": "Represents the % key in screen reader content as a readable word"
},
"caretCharacterDescriptor": {
- "message": "Caret",
+ "message": "Daszek",
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
@@ -4784,7 +4784,7 @@
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
- "message": "Back slash",
+ "message": "Ukośnik wsteczny",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
@@ -4824,7 +4824,7 @@
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {
- "message": "Forward slash",
+ "message": "Ukośnik prawy",
"description": "Represents the / key in screen reader content as a readable word"
},
"lowercaseAriaLabel": {
@@ -4852,7 +4852,7 @@
"message": "Bitwarden wyśle kod na Twój adres e-mail w celu zweryfikowania logowania z nowych urządzeń, począwszy od lutego 2025 r."
},
"newDeviceVerificationNoticeContentPage2": {
- "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail do którego masz dostęp."
+ "message": "Możesz skonfigurować dwustopniowe logowanie jako alternatywny sposób ochrony konta lub zmienić swój adres e-mail, do którego masz dostęp."
},
"remindMeLater": {
"message": "Przypomnij mi później"
@@ -4887,10 +4887,19 @@
"extraWide": {
"message": "Bardzo szerokie"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Nie można usunąć kolekcji z uprawnieniami tylko do przeglądania: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
- "message": "Please update your desktop application"
+ "message": "Zaktualizuj aplikację na komputer"
},
"updateDesktopAppOrDisableFingerprintDialogMessage": {
- "message": "To use biometric unlock, please update your desktop application, or disable fingerprint unlock in the desktop settings."
+ "message": "Aby używać odblokowywania biometrycznego, zaktualizuj aplikację na komputerze lub wyłącz odblokowywanie odciskiem palca w ustawieniach aplikacji na komputerze."
}
}
diff --git a/apps/browser/src/_locales/pt_BR/messages.json b/apps/browser/src/_locales/pt_BR/messages.json
index 2de90042386..a383a450358 100644
--- a/apps/browser/src/_locales/pt_BR/messages.json
+++ b/apps/browser/src/_locales/pt_BR/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Uma notificação foi enviada para seu dispositivo."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Uma notificação foi enviada para o seu dispositivo"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Certifique-se que sua conta esteja desbloqueada e que a frase de identificação corresponda à do outro dispositivo"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Você será notificado assim que a requisição for aprovada"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login iniciado"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Senha mestra comprometida"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Nome do item"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "A organização está desativada"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra Grande"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Você não pode remover coleções com permissões de Somente leitura: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/pt_PT/messages.json b/apps/browser/src/_locales/pt_PT/messages.json
index 706e39bff9a..da0f34a1166 100644
--- a/apps/browser/src/_locales/pt_PT/messages.json
+++ b/apps/browser/src/_locales/pt_PT/messages.json
@@ -296,7 +296,7 @@
"message": "Continuar para a loja de extensões do navegador?"
},
"continueToBrowserExtensionStoreDesc": {
- "message": "Ajude outras pessoas a descobrir se o Bitwarden lhes é adequado. Visite a loja de extensões do seu navegador e deixe uma avaliação agora."
+ "message": "Ajude outras pessoas a descobrir se o Bitwarden lhes é adequado. Visite a loja de extensões do seu navegador e deixe uma classificação agora."
},
"changeMasterPasswordOnWebConfirmation": {
"message": "Pode alterar a sua palavra-passe mestra na aplicação Web Bitwarden."
@@ -340,7 +340,7 @@
"message": "Gestor de Segredos Bitwarden"
},
"continueToSecretsManagerPageDesc": {
- "message": "Armazene, gira e partilhe segredos de programador de forma segura com o Gestor de Segredos Bitwarden. Saiba mais no site bitwarden.com."
+ "message": "Armazene, faça a gestão e partilhe de forma segura os segredos dos programadores com o Gestor de Segredos Bitwarden. Saiba mais no site bitwarden.com."
},
"passwordlessDotDev": {
"message": "Passwordless.dev"
@@ -379,7 +379,7 @@
"message": "Nome da pasta"
},
"folderHintText": {
- "message": "Aninhe uma pasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns"
+ "message": "Crie uma subpasta adicionando o nome da pasta principal seguido de um \"/\". Exemplo: Redes Sociais/Fóruns"
},
"noFoldersAdded": {
"message": "Nenhuma pasta adicionada"
@@ -651,7 +651,7 @@
"message": "Outras opções"
},
"rateExtension": {
- "message": "Avaliar a extensão"
+ "message": "Classificar a extensão"
},
"browserNotSupportClipboard": {
"message": "O seu navegador Web não suporta a cópia fácil da área de transferência. Em vez disso, copie manualmente."
@@ -1598,7 +1598,7 @@
"message": "Booleano"
},
"cfTypeCheckbox": {
- "message": "Checkbox"
+ "message": "Caixa de verificação"
},
"cfTypeLinked": {
"message": "Associado",
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Foi enviada uma notificação para o seu dispositivo."
},
+ "notificationSentDevicePart1": {
+ "message": "Desbloqueie o Bitwarden no seu dispositivo ou no "
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "aplicação web"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Certifique-se de que a frase da impressão digital corresponde à frase abaixo indicada antes de a aprovar."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Foi enviada uma notificação para o seu dispositivo"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Certifique-se de que a sua conta está desbloqueada e que a frase de impressão digital corresponde à do outro dispositivo"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Será notificado quando o pedido for aprovado"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "A preparar o início de sessão"
},
+ "logInRequestSent": {
+ "message": "Pedido enviado"
+ },
"exposedMasterPassword": {
"message": "Palavra-passe mestra exposta"
},
@@ -3208,7 +3217,7 @@
"message": "O atalho de preenchimento automático de credenciais não está definido. Altere-o nas definições do navegador."
},
"autofillLoginShortcutText": {
- "message": "O atalho de preenchimento automático de credenciais é $COMMAND$. Gira todos os atalhos nas definidções do navegador.",
+ "message": "O atalho de preenchimento automático de credenciais é $COMMAND$. Organize todos os atalhos nas definições do navegador.",
"placeholders": {
"command": {
"content": "$1",
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Muito ampla"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Não é possível remover coleções com permissões de Apenas visualização: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Por favor, atualize a sua aplicação para computador"
},
diff --git a/apps/browser/src/_locales/ro/messages.json b/apps/browser/src/_locales/ro/messages.json
index 966d2b4e01d..32cb0215916 100644
--- a/apps/browser/src/_locales/ro/messages.json
+++ b/apps/browser/src/_locales/ro/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "O notificare a fost trimisă pe dispozitivul dvs."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Conectare inițiată"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Parolă principală compromisă"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/ru/messages.json b/apps/browser/src/_locales/ru/messages.json
index f0e3b53bfb2..8d390e83332 100644
--- a/apps/browser/src/_locales/ru/messages.json
+++ b/apps/browser/src/_locales/ru/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "На ваше устройство отправлено уведомление."
},
+ "notificationSentDevicePart1": {
+ "message": "Разблокируйте Bitwarden на своем устройстве или"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "веб-приложении"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Перед одобрением убедитесь, что фраза отпечатка совпадает с приведенной ниже."
+ },
"aNotificationWasSentToYourDevice": {
"message": "На ваше устройство было отправлено уведомление"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Убедитесь, что ваш аккаунт разблокирован и фраза отпечатка совпадает с фразой на другом устройстве"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Вы получите уведомление, когда запрос будет одобрен"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Вход инициирован"
},
+ "logInRequestSent": {
+ "message": "Запрос отправлен"
+ },
"exposedMasterPassword": {
"message": "Мастер-пароль скомпрометирован"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Название элемента"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Организация деактивирована"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Очень широкое"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Вы не можете удалить коллекции с правами только на просмотр: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Пожалуйста, обновите приложение для компьютера"
},
diff --git a/apps/browser/src/_locales/si/messages.json b/apps/browser/src/_locales/si/messages.json
index 9bd2006f1b2..bda76601e90 100644
--- a/apps/browser/src/_locales/si/messages.json
+++ b/apps/browser/src/_locales/si/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/sk/messages.json b/apps/browser/src/_locales/sk/messages.json
index d8bbe1645fa..f2b50505e8a 100644
--- a/apps/browser/src/_locales/sk/messages.json
+++ b/apps/browser/src/_locales/sk/messages.json
@@ -2902,7 +2902,7 @@
"message": "Služba"
},
"forwardedEmail": {
- "message": "Alias preposlaného e-mailu"
+ "message": "Alias presmerovaného e-mailu"
},
"forwardedEmailDesc": {
"message": "Vytvoriť e-mailový alias pomocou externej služby preposielania."
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Do vášho zariadenia bolo odoslané upozornenie."
},
+ "notificationSentDevicePart1": {
+ "message": "Odomknúť Bitwarden vo svojom zariadení alebo vo"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "webovej aplikácii"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Pred schválením sa uistite, že sa odtlačok prístupovej frázy zhoduje s tou uvedenou nižšie."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Do vášho zariadenia bolo odoslané upozornenie"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Uistite sa, že je váš účet odomknutý a fráza odtlačku prsta sa zhoduje s frázou na druhom zariadení"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Po schválení žiadosti budete informovaní"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Iniciované prihlásenie"
},
+ "logInRequestSent": {
+ "message": "Požiadavka bola odoslaná"
+ },
"exposedMasterPassword": {
"message": "Odhalené hlavné heslo"
},
@@ -4146,15 +4155,6 @@
"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á"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra široké"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Zbierky, ktoré môžete len zobraziť nemôžete odstrániť: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Aktualizujte desktopovú aplikáciu"
},
diff --git a/apps/browser/src/_locales/sl/messages.json b/apps/browser/src/_locales/sl/messages.json
index d533c3e02dc..cbff6df98c2 100644
--- a/apps/browser/src/_locales/sl/messages.json
+++ b/apps/browser/src/_locales/sl/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/sr/messages.json b/apps/browser/src/_locales/sr/messages.json
index 3cf637b21f1..cb143049ca3 100644
--- a/apps/browser/src/_locales/sr/messages.json
+++ b/apps/browser/src/_locales/sr/messages.json
@@ -2062,7 +2062,7 @@
"message": "Генератор корисничког имена"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "Користи ову епошту"
},
"useThisPassword": {
"message": "Употреби ову лозинку"
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Обавештење је послато на ваш уређај."
},
+ "notificationSentDevicePart1": {
+ "message": "Откључај Bitwarden на твом уређају или на"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "веб апликација"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Потврдите да се фраза отиска прста поклапа са овом испод пре одобравања."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Обавештење је послато на ваш уређај"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Уверите се да је ваш налог откључан и да се фраза отиска подудара на другом уређају"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Бићете обавештени када захтев буде одобрен"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Пријава је покренута"
},
+ "logInRequestSent": {
+ "message": "Захтев је послат"
+ },
"exposedMasterPassword": {
"message": "Изложена главна лозинка"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Име ставке"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Организација је деактивирана"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Врло широко"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Не можете уклонити колекције са дозволама само за приказ: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Молим вас надоградите вашу апликацију на рачунару"
},
diff --git a/apps/browser/src/_locales/sv/messages.json b/apps/browser/src/_locales/sv/messages.json
index c999454fa9d..727326e1cd6 100644
--- a/apps/browser/src/_locales/sv/messages.json
+++ b/apps/browser/src/_locales/sv/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "En avisering har skickats till din enhet."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Inloggning påbörjad"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Huvudlösenordet har exponerats"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/te/messages.json b/apps/browser/src/_locales/te/messages.json
index eb44a6806d1..800523bdc2e 100644
--- a/apps/browser/src/_locales/te/messages.json
+++ b/apps/browser/src/_locales/te/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/th/messages.json b/apps/browser/src/_locales/th/messages.json
index 991fc80f6d6..04775fa0f91 100644
--- a/apps/browser/src/_locales/th/messages.json
+++ b/apps/browser/src/_locales/th/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "A notification has been sent to your device."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Login initiated"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Exposed Master Password"
},
@@ -4146,15 +4155,6 @@
"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"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/tr/messages.json b/apps/browser/src/_locales/tr/messages.json
index e54944e8222..b6d54e96ecf 100644
--- a/apps/browser/src/_locales/tr/messages.json
+++ b/apps/browser/src/_locales/tr/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Cihazınıza bir bildirim gönderildi."
},
+ "notificationSentDevicePart1": {
+ "message": "Bitwarden kilidini cihazınızdan veya"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web uygulamasından açın"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Onay vermeden önce parmak izi ifadesinin aşağıdakiyle eşleştiğini kontrol edin."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Cihazınıza bir bildirim gönderildi"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Lütfen hesabınızın kilidinin açık olduğundan ve parmak izi ifadesinin diğer cihazla eşleştiğinden emin olun"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "İsteğiniz onaylanınca size haber vereceğiz"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Giriş başlatıldı"
},
+ "logInRequestSent": {
+ "message": "İstek gönderildi"
+ },
"exposedMasterPassword": {
"message": "Açığa Çıkmış Ana Parola"
},
@@ -4146,15 +4155,6 @@
"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ş"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Ekstra geniş"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Lütfen masaüstü uygulamanızı güncelleyin"
},
diff --git a/apps/browser/src/_locales/uk/messages.json b/apps/browser/src/_locales/uk/messages.json
index dfc8f700352..9c9041dd008 100644
--- a/apps/browser/src/_locales/uk/messages.json
+++ b/apps/browser/src/_locales/uk/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Сповіщення було надіслано на ваш пристрій."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "Сповіщення надіслано на ваш пристрій"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Переконайтеся, що ваш обліковий запис розблоковано і фраза відбитка на іншому пристрої збігається"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "Після схвалення запиту ви отримаєте сповіщення"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Ініційовано вхід"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Головний пароль викрито"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Назва запису"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Організацію деактивовано"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Дуже широке"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Ви не можете вилучати збірки, маючи дозвіл лише на перегляд: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Оновіть свою комп'ютерну програму"
},
diff --git a/apps/browser/src/_locales/vi/messages.json b/apps/browser/src/_locales/vi/messages.json
index 9394ebdc8d7..b398b43bab3 100644
--- a/apps/browser/src/_locales/vi/messages.json
+++ b/apps/browser/src/_locales/vi/messages.json
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "Một thông báo đã được gửi đến thiết bị của bạn."
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "web app"
+ },
+ "notificationSentDevicePart2": {
+ "message": "Make sure the Fingerprint phrase matches the one below before approving."
+ },
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "Bắt đầu đăng nhập"
},
+ "logInRequestSent": {
+ "message": "Request sent"
+ },
"exposedMasterPassword": {
"message": "Mật khẩu chính bị lộ"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "Tên mục"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "Bạn không thể xóa các bộ sưu tập với quyền chỉ xem: $COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "Tổ chức không còn hoạt động"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "Extra wide"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "Bạn không thể xóa các bộ sưu tập với quyền chỉ xem: $COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "Please update your desktop application"
},
diff --git a/apps/browser/src/_locales/zh_CN/messages.json b/apps/browser/src/_locales/zh_CN/messages.json
index cc0cc7b8bd2..78783b63729 100644
--- a/apps/browser/src/_locales/zh_CN/messages.json
+++ b/apps/browser/src/_locales/zh_CN/messages.json
@@ -379,7 +379,7 @@
"message": "文件夹名称"
},
"folderHintText": {
- "message": "通过在父文件夹名后面跟随「/」来嵌套文件夹。示例:Social/Forums"
+ "message": "通过在父文件夹名后面添加「/」来嵌套文件夹。示例:Social/Forums"
},
"noFoldersAdded": {
"message": "未添加文件夹"
@@ -591,7 +591,7 @@
"message": "私密备注"
},
"note": {
- "message": "备注"
+ "message": "笔记"
},
"editItem": {
"message": "编辑项目"
@@ -2062,7 +2062,7 @@
"message": "用户名生成器"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "使用此电子邮箱"
},
"useThisPassword": {
"message": "使用此密码"
@@ -2664,7 +2664,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
- "message": "$TOTAL$ 不足",
+ "message": "总计 $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "通知已发送到您的设备。"
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "网页 App"
+ },
+ "notificationSentDevicePart2": {
+ "message": "在批准前,请确保指纹短语与下面的一致。"
+ },
"aNotificationWasSentToYourDevice": {
"message": "通知已发送到您的设备"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "确保您的账户已解锁,并且指纹短语与其他设备上的相匹配。"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "请求获得批准后,您将收到通知"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "登录已发起"
},
+ "logInRequestSent": {
+ "message": "请求已发送"
+ },
"exposedMasterPassword": {
"message": "已暴露的主密码"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "项目名称"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "组织已停用"
},
@@ -4226,7 +4226,7 @@
"message": "筛选"
},
"filterVault": {
- "message": "密码库筛选"
+ "message": "筛选密码库"
},
"filterApplied": {
"message": "已应用一个筛选"
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "超宽"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "您无法删除仅具有「查看」权限的集合:$COLLECTIONS$",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "请更新您的桌面应用程序"
},
diff --git a/apps/browser/src/_locales/zh_TW/messages.json b/apps/browser/src/_locales/zh_TW/messages.json
index f9ef4d56a49..30351228927 100644
--- a/apps/browser/src/_locales/zh_TW/messages.json
+++ b/apps/browser/src/_locales/zh_TW/messages.json
@@ -35,7 +35,7 @@
"message": "設定一個強密碼"
},
"finishCreatingYourAccountBySettingAPassword": {
- "message": "設定密碼以完成創建您的帳戶。"
+ "message": "設定密碼以完成建立您的帳號"
},
"enterpriseSingleSignOn": {
"message": "企業單一登入"
@@ -763,7 +763,7 @@
"message": "若您忘記主密碼,將會無法找回!"
},
"masterPassHintLabel": {
- "message": "您已成功創建新帳戶!"
+ "message": "主密碼提示"
},
"errorOccurred": {
"message": "發生錯誤"
@@ -797,7 +797,7 @@
"message": "帳戶已建立!現在可以登入了。"
},
"newAccountCreated2": {
- "message": "您已成功創建新帳戶!"
+ "message": "您已成功建立新帳號!"
},
"youHaveBeenLoggedIn": {
"message": "你已經登入!"
@@ -2062,7 +2062,7 @@
"message": "使用者名稱產生器"
},
"useThisEmail": {
- "message": "Use this email"
+ "message": "使用此電子郵件"
},
"useThisPassword": {
"message": "使用此密碼"
@@ -2529,7 +2529,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createdSendSuccessfully": {
- "message": "Send 創建成功!",
+ "message": "Send 建立成功!",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInHoursSingle": {
@@ -3123,12 +3123,18 @@
"notificationSentDevice": {
"message": "已傳送通知至您的裝置。"
},
+ "notificationSentDevicePart1": {
+ "message": "Unlock Bitwarden on your device or on the"
+ },
+ "notificationSentDeviceAnchor": {
+ "message": "網頁應用程式"
+ },
+ "notificationSentDevicePart2": {
+ "message": "在核准前請確保您的指紋短語與下面完全相符。"
+ },
"aNotificationWasSentToYourDevice": {
"message": "已傳送通知至您的裝置"
},
- "makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
- "message": "請確保您的帳號已解鎖,並且指紋短語與其他裝置一致。"
- },
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "一旦您的請求被通過,您會獲得通知。"
},
@@ -3138,6 +3144,9 @@
"loginInitiated": {
"message": "登入已啟動"
},
+ "logInRequestSent": {
+ "message": "已傳送請求"
+ },
"exposedMasterPassword": {
"message": "已洩露的主密碼"
},
@@ -4146,15 +4155,6 @@
"itemName": {
"message": "項目名稱"
},
- "cannotRemoveViewOnlyCollections": {
- "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。",
- "placeholders": {
- "collections": {
- "content": "$1",
- "example": "Work, Personal"
- }
- }
- },
"organizationIsDeactivated": {
"message": "組織已被停用"
},
@@ -4887,6 +4887,15 @@
"extraWide": {
"message": "更寬"
},
+ "cannotRemoveViewOnlyCollections": {
+ "message": "若您只有檢視權限,無法移除集合 $COLLECTIONS$。",
+ "placeholders": {
+ "collections": {
+ "content": "$1",
+ "example": "Work, Personal"
+ }
+ }
+ },
"updateDesktopAppOrDisableFingerprintDialogTitle": {
"message": "請更新您的桌面應用程式"
},
diff --git a/apps/browser/src/auth/popup/register.component.html b/apps/browser/src/auth/popup/register.component.html
deleted file mode 100644
index e2f4f2e7d12..00000000000
--- a/apps/browser/src/auth/popup/register.component.html
+++ /dev/null
@@ -1,147 +0,0 @@
-
diff --git a/apps/browser/src/auth/popup/register.component.ts b/apps/browser/src/auth/popup/register.component.ts
deleted file mode 100644
index 50475b2204d..00000000000
--- a/apps/browser/src/auth/popup/register.component.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Component } from "@angular/core";
-import { UntypedFormBuilder } from "@angular/forms";
-import { Router } from "@angular/router";
-
-import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/auth/components/register.component";
-import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
-import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
-import { ApiService } from "@bitwarden/common/abstractions/api.service";
-import { AuditService } from "@bitwarden/common/abstractions/audit.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 { DialogService, ToastService } from "@bitwarden/components";
-import { KeyService } from "@bitwarden/key-management";
-
-@Component({
- selector: "app-register",
- templateUrl: "register.component.html",
-})
-export class RegisterComponent extends BaseRegisterComponent {
- color: string;
- text: string;
-
- constructor(
- formValidationErrorService: FormValidationErrorsService,
- formBuilder: UntypedFormBuilder,
- loginStrategyService: LoginStrategyServiceAbstraction,
- router: Router,
- i18nService: I18nService,
- keyService: KeyService,
- apiService: ApiService,
- platformUtilsService: PlatformUtilsService,
- environmentService: EnvironmentService,
- logService: LogService,
- auditService: AuditService,
- dialogService: DialogService,
- toastService: ToastService,
- ) {
- super(
- formValidationErrorService,
- formBuilder,
- loginStrategyService,
- router,
- i18nService,
- keyService,
- apiService,
- platformUtilsService,
- environmentService,
- logService,
- auditService,
- dialogService,
- toastService,
- );
- }
-}
diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts
index 37c05a55a3a..40c4d07cadf 100644
--- a/apps/browser/src/autofill/background/notification.background.spec.ts
+++ b/apps/browser/src/autofill/background/notification.background.spec.ts
@@ -825,6 +825,7 @@ describe("NotificationBackground", () => {
queueMessage.newPassword,
message.edit,
sender.tab,
+ "testId",
);
expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
@@ -862,6 +863,7 @@ describe("NotificationBackground", () => {
queueMessage.password,
message.edit,
sender.tab,
+ "testId",
);
expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled();
@@ -895,6 +897,7 @@ describe("NotificationBackground", () => {
queueMessage.newPassword,
message.edit,
sender.tab,
+ "testId",
);
expect(editItemSpy).toHaveBeenCalled();
expect(updateWithServerSpy).not.toHaveBeenCalled();
@@ -904,10 +907,13 @@ describe("NotificationBackground", () => {
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "editedCipher",
});
- expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({
- cipher: cipherView,
- collectionIds: cipherView.collectionIds,
- });
+ expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith(
+ {
+ cipher: cipherView,
+ collectionIds: cipherView.collectionIds,
+ },
+ "testId",
+ );
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, {
cipherId: cipherView.id,
});
@@ -945,7 +951,7 @@ describe("NotificationBackground", () => {
queueMessage,
message.folder,
);
- expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab);
+ expect(editItemSpy).toHaveBeenCalledWith(cipherView, "testId", sender.tab);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar",
});
diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts
index ad3bee97d8a..1a99425b7de 100644
--- a/apps/browser/src/autofill/background/notification.background.ts
+++ b/apps/browser/src/autofill/background/notification.background.ts
@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import {
ExtensionCommand,
ExtensionCommandType,
@@ -22,9 +23,11 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
+import { buildCipherIcon } from "@bitwarden/common/vault/icon/build-cipher-icon";
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";
@@ -32,6 +35,7 @@ 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 { openAddEditVaultItemPopout } from "../../vault/popup/utils/vault-popout-window";
+import { NotificationCipherData } from "../content/components/cipher/types";
import { NotificationQueueMessageType } from "../enums/notification-queue-message-type.enum";
import { AutofillService } from "../services/abstractions/autofill.service";
@@ -82,10 +86,9 @@ export default class NotificationBackground {
bgGetActiveUserServerConfig: () => this.getActiveUserServerConfig(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
notificationRefreshFlagValue: () => this.getNotificationFlag(),
+ bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
};
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
constructor(
private autofillService: AutofillService,
private cipherService: CipherService,
@@ -132,6 +135,46 @@ export default class NotificationBackground {
return await firstValueFrom(this.domainSettingsService.neverDomains$);
}
+ /**
+ *
+ * Gets the current active tab and retrieves all decrypted ciphers
+ * for the tab's URL. It constructs and returns an array of `NotificationCipherData` objects.
+ * If no active tab or URL is found, it returns an empty array.
+ *
+ * @returns {Promise}
+ */
+
+ async getNotificationCipherData(): Promise {
+ const [currentTab, showFavicons, env] = await Promise.all([
+ BrowserApi.getTabFromCurrentWindow(),
+ firstValueFrom(this.domainSettingsService.showFavicons$),
+ firstValueFrom(this.environmentService.environment$),
+ ]);
+ const iconsServerUrl = env.getIconsUrl();
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl(
+ currentTab.url,
+ activeUserId,
+ );
+
+ return decryptedCiphers.map((view) => {
+ const { id, name, reprompt, favorite, login } = view;
+ return {
+ id,
+ name,
+ type: CipherType.Login,
+ reprompt,
+ favorite,
+ icon: buildCipherIcon(iconsServerUrl, view, showFavicons),
+ login: login && {
+ username: login.username,
+ },
+ };
+ });
+ }
+
/**
* Gets the active user server config from the config service.
*/
@@ -267,7 +310,14 @@ export default class NotificationBackground {
return;
}
- const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return;
+ }
+
+ const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId);
const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
);
@@ -345,7 +395,14 @@ export default class NotificationBackground {
}
let id: string = null;
- const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return;
+ }
+
+ const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId);
if (changeData.currentPassword != null) {
const passwordMatches = ciphers.filter(
(c) => c.login.password === changeData.currentPassword,
@@ -498,37 +555,42 @@ export default class NotificationBackground {
this.notificationQueue.splice(i, 1);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
- const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId);
- await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab);
+ const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
+ await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab, activeUserId);
return;
}
// If the vault was locked, check if a cipher needs updating instead of creating a new one
if (queueMessage.wasVaultLocked) {
- const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri);
+ const allCiphers = await this.cipherService.getAllDecryptedForUrl(
+ queueMessage.uri,
+ activeUserId,
+ );
const existingCipher = allCiphers.find(
(c) =>
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username,
);
if (existingCipher != null) {
- await this.updatePassword(existingCipher, queueMessage.password, edit, tab);
+ await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId);
return;
}
}
- folderId = (await this.folderExists(folderId)) ? folderId : null;
+ folderId = (await this.folderExists(folderId, activeUserId)) ? folderId : null;
const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId);
if (edit) {
- await this.editItem(newCipher, tab);
+ await this.editItem(newCipher, activeUserId, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
return;
}
- const activeUserId = await firstValueFrom(this.activeUserId$);
-
const cipher = await this.cipherService.encrypt(newCipher, activeUserId);
try {
await this.cipherService.createWithServer(cipher);
@@ -551,24 +613,25 @@ export default class NotificationBackground {
* @param newPassword - The new password to update the cipher with
* @param edit - Identifies if the cipher should be edited or simply updated
* @param tab - The tab that the message was sent from
+ * @param userId - The active account user ID
*/
private async updatePassword(
cipherView: CipherView,
newPassword: string,
edit: boolean,
tab: chrome.tabs.Tab,
+ userId: UserId,
) {
cipherView.login.password = newPassword;
if (edit) {
- await this.editItem(cipherView, tab);
+ await this.editItem(cipherView, userId, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
await BrowserApi.tabSendMessage(tab, { command: "editedCipher" });
return;
}
- const activeUserId = await firstValueFrom(this.activeUserId$);
- const cipher = await this.cipherService.encrypt(cipherView, activeUserId);
+ const cipher = await this.cipherService.encrypt(cipherView, userId);
try {
// We've only updated the password, no need to broadcast editedCipher message
await this.cipherService.updateWithServer(cipher);
@@ -585,33 +648,34 @@ export default class NotificationBackground {
* and opens the add/edit vault item popout.
*
* @param cipherView - The cipher to edit
+ * @param userId - The active account user ID
* @param senderTab - The tab that the message was sent from
*/
- private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) {
- await this.cipherService.setAddEditCipherInfo({
- cipher: cipherView,
- collectionIds: cipherView.collectionIds,
- });
+ private async editItem(cipherView: CipherView, userId: UserId, senderTab: chrome.tabs.Tab) {
+ await this.cipherService.setAddEditCipherInfo(
+ {
+ cipher: cipherView,
+ collectionIds: cipherView.collectionIds,
+ },
+ userId,
+ );
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id });
}
- private async folderExists(folderId: string) {
+ private async folderExists(folderId: string, userId: UserId) {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false;
}
- const activeUserId = await firstValueFrom(this.activeUserId$);
- const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId));
+ const folders = await firstValueFrom(this.folderService.folderViews$(userId));
return folders.some((x) => x.id === folderId);
}
- private async getDecryptedCipherById(cipherId: string) {
- const cipher = await this.cipherService.get(cipherId);
+ private async getDecryptedCipherById(cipherId: string, userId: UserId) {
+ const cipher = await this.cipherService.get(cipherId, userId);
if (cipher != null && cipher.type === CipherType.Login) {
- const activeUserId = await firstValueFrom(this.activeUserId$);
-
return await cipher.decrypt(
- await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
+ await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
}
return null;
@@ -648,7 +712,9 @@ export default class NotificationBackground {
* Returns the first value found from the folder service's folderViews$ observable.
*/
private async getFolderData() {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
return await firstValueFrom(this.folderService.folderViews$(activeUserId));
}
diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts
index c3a6357ed05..22531788d37 100644
--- a/apps/browser/src/autofill/background/overlay.background.spec.ts
+++ b/apps/browser/src/autofill/background/overlay.background.spec.ts
@@ -206,6 +206,7 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService,
themeStateService,
totpService,
+ accountService,
generatedPasswordCallbackMock,
addPasswordCallbackMock,
);
@@ -849,7 +850,7 @@ describe("OverlayBackground", () => {
await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [
CipherType.Card,
CipherType.Identity,
]);
@@ -872,7 +873,7 @@ describe("OverlayBackground", () => {
await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId);
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([
@@ -891,7 +892,7 @@ describe("OverlayBackground", () => {
await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [
CipherType.Card,
CipherType.Identity,
]);
diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts
index 3d2b1ec783c..1d55a154ee3 100644
--- a/apps/browser/src/autofill/background/overlay.background.ts
+++ b/apps/browser/src/autofill/background/overlay.background.ts
@@ -13,8 +13,10 @@ import {
} from "rxjs";
import { parse } from "tldts";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service";
import {
AutofillOverlayVisibility,
SHOW_AUTOFILL_BUTTON,
@@ -34,6 +36,7 @@ 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 { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
@@ -225,6 +228,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private themeStateService: ThemeStateService,
private totpService: TotpService,
+ private accountService: AccountService,
private generatePasswordCallback: () => Promise,
private addPasswordCallback: (password: string) => Promise,
) {
@@ -405,13 +409,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
currentTab: chrome.tabs.Tab,
updateAllCipherTypes: boolean,
): Promise {
- if (updateAllCipherTypes || !this.cardAndIdentityCiphers) {
- return this.getAllCipherTypeViews(currentTab);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (!activeUserId) {
+ return [];
}
- const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort(
- (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b),
- );
+ if (updateAllCipherTypes || !this.cardAndIdentityCiphers) {
+ return this.getAllCipherTypeViews(currentTab, activeUserId);
+ }
+
+ const cipherViews = (
+ await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", activeUserId)
+ ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
return this.cardAndIdentityCiphers
? cipherViews.concat(...this.cardAndIdentityCiphers)
@@ -422,15 +433,19 @@ export class OverlayBackground implements OverlayBackgroundInterface {
* Queries all cipher types from the user's vault returns them sorted by last used.
*
* @param currentTab - The current tab
+ * @param userId - The active user id
*/
- private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise {
+ private async getAllCipherTypeViews(
+ currentTab: chrome.tabs.Tab,
+ userId: UserId,
+ ): Promise {
if (!this.cardAndIdentityCiphers) {
this.cardAndIdentityCiphers = new Set([]);
}
this.cardAndIdentityCiphers.clear();
const cipherViews = (
- await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [
+ await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", userId, [
CipherType.Card,
CipherType.Identity,
])
@@ -2399,10 +2414,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
try {
this.closeInlineMenu(sender);
- await this.cipherService.setAddEditCipherInfo({
- cipher: cipherView,
- collectionIds: cipherView.collectionIds,
- });
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.cipherService.setAddEditCipherInfo(
+ {
+ cipher: cipherView,
+ collectionIds: cipherView.collectionIds,
+ },
+ activeUserId,
+ );
await this.openAddEditVaultItemPopout(sender.tab, {
cipherId: cipherView.id,
diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts
index 2c14358a359..22e10a3dd0a 100644
--- a/apps/browser/src/autofill/background/web-request.background.ts
+++ b/apps/browser/src/autofill/background/web-request.background.ts
@@ -1,7 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
+import { firstValueFrom } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -14,6 +18,7 @@ export default class WebRequestBackground {
platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private authService: AuthService,
+ private accountService: AccountService,
private readonly webRequest: typeof chrome.webRequest,
) {
this.isFirefox = platformUtilsService.isFirefox();
@@ -55,7 +60,16 @@ export default class WebRequestBackground {
// eslint-disable-next-line
private async resolveAuthCredentials(domain: string, success: Function, error: Function) {
- if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) {
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ error();
+ return;
+ }
+
+ const authStatus = await firstValueFrom(this.authService.authStatusFor$(activeUserId));
+ if (authStatus < AuthenticationStatus.Unlocked) {
error();
return;
}
@@ -63,6 +77,7 @@ export default class WebRequestBackground {
try {
const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain,
+ activeUserId,
null,
UriMatchStrategy.Host,
);
diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts
index 4fed9eee5ef..3228aed4688 100644
--- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts
+++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts
@@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@@ -14,6 +16,9 @@ describe("CipherContextMenuHandler", () => {
let authService: MockProxy;
let cipherService: MockProxy;
+ const mockUserId = "UserId" as UserId;
+ const accountService = mockAccountServiceWith(mockUserId);
+
let sut: CipherContextMenuHandler;
beforeEach(() => {
@@ -24,7 +29,12 @@ describe("CipherContextMenuHandler", () => {
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
- sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService);
+ sut = new CipherContextMenuHandler(
+ mainContextMenuHandler,
+ authService,
+ cipherService,
+ accountService,
+ );
});
afterEach(() => jest.resetAllMocks());
@@ -119,10 +129,11 @@ describe("CipherContextMenuHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", [
- CipherType.Card,
- CipherType.Identity,
- ]);
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
+ "https://test.com",
+ mockUserId,
+ [CipherType.Card, CipherType.Identity],
+ );
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3);
diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
index b112ff00efe..038f4e85c9a 100644
--- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
+++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts
@@ -1,5 +1,9 @@
+import { firstValueFrom } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -14,6 +18,7 @@ export class CipherContextMenuHandler {
private mainContextMenuHandler: MainContextMenuHandler,
private authService: AuthService,
private cipherService: CipherService,
+ private accountService: AccountService,
) {}
async update(url: string) {
@@ -35,7 +40,14 @@ export class CipherContextMenuHandler {
return;
}
- const ciphers = await this.cipherService.getAllDecryptedForUrl(url, [
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return;
+ }
+
+ const ciphers = await this.cipherService.getAllDecryptedForUrl(url, activeUserId, [
CipherType.Card,
CipherType.Identity,
]);
diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
index 6ef004f7979..c8cb7e81f72 100644
--- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
+++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts
@@ -61,6 +61,8 @@ describe("ContextMenuClickedHandler", () => {
return cipherView;
};
+ const mockUserId = "UserId" as UserId;
+
let copyToClipboard: CopyToClipboardAction;
let generatePasswordToClipboard: GeneratePasswordToClipboardAction;
let autofill: AutofillAction;
@@ -79,7 +81,7 @@ describe("ContextMenuClickedHandler", () => {
autofill = jest.fn, [tab: chrome.tabs.Tab, cipher: CipherView]>();
authService = mock();
cipherService = mock();
- accountService = mockAccountServiceWith("userId" as UserId);
+ accountService = mockAccountServiceWith(mockUserId as UserId);
totpService = mock();
eventCollectionService = mock();
@@ -191,7 +193,11 @@ describe("ContextMenuClickedHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
+ "https://test.com",
+ mockUserId,
+ [],
+ );
expect(copyToClipboard).toHaveBeenCalledTimes(1);
@@ -215,7 +221,11 @@ describe("ContextMenuClickedHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []);
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
+ "https://test.com",
+ mockUserId,
+ [],
+ );
});
});
});
diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
index 597d75575b0..69c8b6e70b8 100644
--- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
+++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts
@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import {
AUTOFILL_CARD_ID,
AUTOFILL_ID,
@@ -105,6 +106,13 @@ export class ContextMenuClickedHandler {
menuItemId as string,
);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return;
+ }
+
if (isCreateCipherAction) {
// pass; defer to logic below
} else if (menuItemId === NOOP_COMMAND_SUFFIX) {
@@ -120,12 +128,13 @@ export class ContextMenuClickedHandler {
// in scenarios like unlock on autofill
const ciphers = await this.cipherService.getAllDecryptedForUrl(
tab.url,
+ activeUserId,
additionalCiphersToGet,
);
cipher = ciphers[0];
} else {
- const ciphers = await this.cipherService.getAllDecrypted();
+ const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
cipher = ciphers.find(({ id }) => id === menuItemId);
}
@@ -133,9 +142,6 @@ export class ContextMenuClickedHandler {
return;
}
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
await this.accountService.setAccountActivity(activeUserId, new Date());
switch (info.parentMenuItemId) {
case AUTOFILL_ID:
diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
index de374b44a97..6ff32353938 100644
--- a/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
+++ b/apps/browser/src/autofill/content/components/cipher/cipher-info.ts
@@ -6,10 +6,10 @@ import { Theme } from "@bitwarden/common/platform/enums";
import { themes, typography } from "../../../content/components/constants/styles";
import { CipherInfoIndicatorIcons } from "./cipher-indicator-icons";
-import { CipherData } from "./types";
+import { NotificationCipherData } from "./types";
// @TODO support other cipher types (card, identity, notes, etc)
-export function CipherInfo({ cipher, theme }: { cipher: CipherData; theme: Theme }) {
+export function CipherInfo({ cipher, theme }: { cipher: NotificationCipherData; theme: Theme }) {
const { name, login } = cipher;
return html`
diff --git a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts
index 651c20cac3a..96b44d2c0cc 100644
--- a/apps/browser/src/autofill/content/components/cipher/cipher-item.ts
+++ b/apps/browser/src/autofill/content/components/cipher/cipher-item.ts
@@ -12,7 +12,7 @@ import {
import { CipherAction } from "./cipher-action";
import { CipherIcon } from "./cipher-icon";
import { CipherInfo } from "./cipher-info";
-import { CipherData } from "./types";
+import { NotificationCipherData } from "./types";
const cipherIconWidth = "24px";
@@ -22,7 +22,7 @@ export function CipherItem({
notificationType,
theme = ThemeTypes.Light,
}: {
- cipher: CipherData;
+ cipher: NotificationCipherData;
handleAction?: (e: Event) => void;
notificationType?: NotificationType;
theme: Theme;
diff --git a/apps/browser/src/autofill/content/components/cipher/types.ts b/apps/browser/src/autofill/content/components/cipher/types.ts
index acdee756570..ff29f9b559f 100644
--- a/apps/browser/src/autofill/content/components/cipher/types.ts
+++ b/apps/browser/src/autofill/content/components/cipher/types.ts
@@ -1,6 +1,4 @@
-// FIXME: Remove when updating file. Eslint update
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const CipherTypes = {
+export const CipherTypes = {
Login: 1,
SecureNote: 2,
Card: 3,
@@ -9,9 +7,7 @@ const CipherTypes = {
type CipherType = (typeof CipherTypes)[keyof typeof CipherTypes];
-// FIXME: Remove when updating file. Eslint update
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-const CipherRepromptTypes = {
+export const CipherRepromptTypes = {
None: 0,
Password: 1,
} as const;
@@ -25,13 +21,16 @@ export type WebsiteIconData = {
icon: string;
};
-export type CipherData = {
+type BaseCipherData = {
id: string;
name: string;
- type: CipherType;
+ type: CipherTypeValue;
reprompt: CipherRepromptType;
favorite: boolean;
icon: WebsiteIconData;
+};
+
+export type CipherData = BaseCipherData & {
accountCreationFieldType?: string;
login?: {
username: string;
@@ -46,3 +45,9 @@ export type CipherData = {
username?: string;
};
};
+
+export type NotificationCipherData = BaseCipherData & {
+ login?: {
+ username: string;
+ };
+};
diff --git a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts
index 90616647b0e..c4b32e8b0f1 100644
--- a/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts
+++ b/apps/browser/src/autofill/content/components/lit-stories/notification/body.lit-stories.ts
@@ -5,11 +5,11 @@ import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { NotificationType } from "../../../../notification/abstractions/notification-bar";
-import { CipherData } from "../../cipher/types";
+import { NotificationCipherData } from "../../cipher/types";
import { NotificationBody } from "../../notification/body";
type Args = {
- ciphers: CipherData[];
+ ciphers: NotificationCipherData[];
notificationType: NotificationType;
theme: Theme;
};
@@ -38,7 +38,7 @@ export default {
fallbackImage: "https://example.com/fallback.png",
icon: "icon-class",
},
- login: { username: "user@example.com", passkey: null },
+ login: { username: "user@example.com" },
},
],
theme: ThemeTypes.Light,
diff --git a/apps/browser/src/autofill/content/components/notification/body.ts b/apps/browser/src/autofill/content/components/notification/body.ts
index 6a3ed2e5d1e..6dc957ab8b8 100644
--- a/apps/browser/src/autofill/content/components/notification/body.ts
+++ b/apps/browser/src/autofill/content/components/notification/body.ts
@@ -5,7 +5,7 @@ import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums";
import { NotificationType } from "../../../notification/abstractions/notification-bar";
import { CipherItem } from "../cipher";
-import { CipherData } from "../cipher/types";
+import { NotificationCipherData } from "../cipher/types";
import { scrollbarStyles, spacing, themes, typography } from "../constants/styles";
import { ItemRow } from "../rows/item-row";
@@ -20,7 +20,7 @@ export function NotificationBody({
notificationType,
theme = ThemeTypes.Light,
}: {
- ciphers: CipherData[];
+ ciphers: NotificationCipherData[];
customClasses?: string[];
notificationType?: NotificationType;
theme: Theme;
diff --git a/apps/browser/src/autofill/content/components/notification/container.ts b/apps/browser/src/autofill/content/components/notification/container.ts
index 8bd07ab8296..8d1b57c80cd 100644
--- a/apps/browser/src/autofill/content/components/notification/container.ts
+++ b/apps/browser/src/autofill/content/components/notification/container.ts
@@ -8,8 +8,7 @@ import {
NotificationTypes,
NotificationType,
} from "../../../notification/abstractions/notification-bar";
-import { createAutofillOverlayCipherDataMock } from "../../../spec/autofill-mocks";
-import { CipherData } from "../cipher/types";
+import { NotificationCipherData } from "../cipher/types";
import { themes, spacing } from "../constants/styles";
import { NotificationBody, componentClassPrefix as notificationBodyClassPrefix } from "./body";
@@ -24,23 +23,15 @@ export function NotificationContainer({
i18n,
theme = ThemeTypes.Light,
type,
+ ciphers,
}: NotificationBarIframeInitData & { handleCloseNotification: (e: Event) => void } & {
i18n: { [key: string]: string };
type: NotificationType; // @TODO typing override for generic `NotificationBarIframeInitData.type`
+ ciphers: NotificationCipherData[];
}) {
const headerMessage = getHeaderMessage(i18n, type);
const showBody = true;
- // @TODO remove mock ciphers for development
- const ciphers = [
- createAutofillOverlayCipherDataMock(1),
- { ...createAutofillOverlayCipherDataMock(2), icon: { imageEnabled: false } },
- {
- ...createAutofillOverlayCipherDataMock(3),
- icon: { imageEnabled: true, image: "https://localhost:8443/icons/webtests.dev/icon.png" },
- },
- ] as CipherData[];
-
return html`
${NotificationHeader({
diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
index 2c22097f3d0..497664542ad 100644
--- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
+++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts
@@ -110,6 +110,7 @@ describe("OverlayBackground", () => {
i18nService,
platformUtilsService,
themeStateService,
+ accountService,
);
jest
@@ -205,7 +206,7 @@ describe("OverlayBackground", () => {
await overlayBackground.updateOverlayCiphers();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
- expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url);
+ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId);
expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual(
new Map([
diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
index 5dfade0f863..d0fad4cd00e 100644
--- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
+++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts
@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom } from "rxjs";
+import { firstValueFrom, map } from "rxjs";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
@@ -106,6 +107,7 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private themeStateService: ThemeStateService,
+ private accountService: AccountService,
) {}
/**
@@ -152,9 +154,13 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
}
this.overlayLoginCiphers = new Map();
- const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort(
- (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b),
+
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
+ const ciphersViews = (
+ await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId)
+ ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
}
@@ -660,10 +666,16 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
cipherView.type = CipherType.Login;
cipherView.login = loginView;
- await this.cipherService.setAddEditCipherInfo({
- cipher: cipherView,
- collectionIds: cipherView.collectionIds,
- });
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ );
+ await this.cipherService.setAddEditCipherInfo(
+ {
+ cipher: cipherView,
+ collectionIds: cipherView.collectionIds,
+ },
+ activeUserId,
+ );
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
diff --git a/apps/browser/src/autofill/notification/bar.ts b/apps/browser/src/autofill/notification/bar.ts
index 202b144258d..2316df19857 100644
--- a/apps/browser/src/autofill/notification/bar.ts
+++ b/apps/browser/src/autofill/notification/bar.ts
@@ -84,23 +84,25 @@ function initNotificationBar(message: NotificationBarWindowMessage) {
document.body.innerHTML = "";
// Current implementations utilize a require for scss files which creates the need to remove the node.
document.head.querySelectorAll('link[rel="stylesheet"]').forEach((node) => node.remove());
-
const themeType = getTheme(globalThis, theme);
// There are other possible passed theme values, but for now, resolve to dark or light
const resolvedTheme: Theme = themeType === ThemeTypes.Dark ? ThemeTypes.Dark : ThemeTypes.Light;
- // @TODO use context to avoid prop drilling
- return render(
- NotificationContainer({
- ...notificationBarIframeInitData,
- type: notificationBarIframeInitData.type as NotificationType,
- theme: resolvedTheme,
- handleCloseNotification,
- i18n,
- }),
- document.body,
- );
+ sendPlatformMessage({ command: "bgGetDecryptedCiphers" }, (cipherData) => {
+ // @TODO use context to avoid prop drilling
+ return render(
+ NotificationContainer({
+ ...notificationBarIframeInitData,
+ type: notificationBarIframeInitData.type as NotificationType,
+ theme: resolvedTheme,
+ handleCloseNotification,
+ i18n,
+ ciphers: cipherData,
+ }),
+ document.body,
+ );
+ });
}
setNotificationBarTheme();
diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts
index 4555d87f249..24c14c98685 100644
--- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts
+++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts
@@ -20,6 +20,7 @@ import {
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -186,7 +187,10 @@ export class Fido2Component implements OnInit, OnDestroy {
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
- this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getUserId),
+ );
+ this.ciphers = (await this.cipherService.getAllDecrypted(activeUserId)).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
);
@@ -211,7 +215,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => {
- const cipher = await this.cipherService.get(cipherId);
+ const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
@@ -232,7 +236,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => {
- const cipher = await this.cipherService.get(cipherId);
+ const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts
index 378521cfc42..3843734ad64 100644
--- a/apps/browser/src/autofill/services/autofill.service.spec.ts
+++ b/apps/browser/src/autofill/services/autofill.service.spec.ts
@@ -747,7 +747,7 @@ describe("AutofillService", () => {
jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info");
- jest.spyOn(chrome.runtime, "sendMessage");
+ jest.spyOn(cipherService, "updateLastUsedDate");
jest.spyOn(eventCollectionService, "collect");
const autofillResult = await autofillService.doAutoFill(autofillOptions);
@@ -769,10 +769,10 @@ describe("AutofillService", () => {
);
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled();
expect(logService.info).not.toHaveBeenCalled();
- expect(chrome.runtime.sendMessage).toHaveBeenCalledWith({
- cipherId: autofillOptions.cipher.id,
- command: "updateLastUsedDate",
- });
+ expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith(
+ autofillOptions.cipher.id,
+ mockUserId,
+ );
expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
autofillOptions.pageDetails[0].tab.id,
{
@@ -893,11 +893,11 @@ describe("AutofillService", () => {
it("skips updating the cipher's last used date if the passed options indicate that we should skip the last used cipher", async () => {
autofillOptions.skipLastUsed = true;
- jest.spyOn(chrome.runtime, "sendMessage");
+ jest.spyOn(cipherService, "updateLastUsedDate");
await autofillService.doAutoFill(autofillOptions);
- expect(chrome.runtime.sendMessage).not.toHaveBeenCalled();
+ expect(cipherService.updateLastUsedDate).not.toHaveBeenCalled();
});
it("returns early if the fillScript cannot be generated", async () => {
@@ -1033,8 +1033,8 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, false);
expect(cipherService.getNextCipherForUrl).not.toHaveBeenCalled();
- expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
- expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true);
+ expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
+ expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
});
@@ -1047,7 +1047,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
- expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
+ expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId);
expect(cipherService.getLastLaunchedForUrl).not.toHaveBeenCalled();
expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
@@ -1077,7 +1077,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
- expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
+ expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled();
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
@@ -1107,8 +1107,8 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
- expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true);
- expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true);
+ expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
+ expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
@@ -1135,7 +1135,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand);
- expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
+ expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId);
expect(cipherService.updateLastUsedIndexForUrl).toHaveBeenCalledWith(tab.url);
expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab,
@@ -1166,7 +1166,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
- expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
+ expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId);
expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, {
cipherId: cipher.id,
@@ -1192,7 +1192,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true);
- expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url);
+ expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId);
expect(autofillService["openVaultItemPasswordRepromptPopout"]).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull();
diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts
index c35b19990cb..fc7d0ebcc99 100644
--- a/apps/browser/src/autofill/services/autofill.service.ts
+++ b/apps/browser/src/autofill/services/autofill.service.ts
@@ -8,6 +8,7 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import {
AutofillOverlayVisibility,
CardExpiryDateDelimiters,
@@ -463,13 +464,8 @@ export default class AutofillService implements AutofillServiceInterface {
fillScript.properties.delay_between_operations = 20;
didAutofill = true;
-
if (!options.skipLastUsed) {
- // In order to prevent a UI update send message to background script to update last used date
- await chrome.runtime.sendMessage({
- command: "updateLastUsedDate",
- cipherId: options.cipher.id,
- });
+ await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id);
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -532,17 +528,29 @@ export default class AutofillService implements AutofillServiceInterface {
autoSubmitLogin = false,
): Promise
{
let cipher: CipherView;
+
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return null;
+ }
+
if (fromCommand) {
- cipher = await this.cipherService.getNextCipherForUrl(tab.url);
+ cipher = await this.cipherService.getNextCipherForUrl(tab.url, activeUserId);
} else {
- const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(tab.url, true);
+ const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(
+ tab.url,
+ activeUserId,
+ true,
+ );
if (
lastLaunchedCipher &&
Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000
) {
cipher = lastLaunchedCipher;
} else {
- cipher = await this.cipherService.getLastUsedForUrl(tab.url, true);
+ cipher = await this.cipherService.getLastUsedForUrl(tab.url, activeUserId, true);
}
}
@@ -631,12 +639,19 @@ export default class AutofillService implements AutofillServiceInterface {
let cipher: CipherView;
let cacheKey = "";
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (activeUserId == null) {
+ return null;
+ }
+
if (cipherType === CipherType.Card) {
cacheKey = "cardCiphers";
- cipher = await this.cipherService.getNextCardCipher();
+ cipher = await this.cipherService.getNextCardCipher(activeUserId);
} else {
cacheKey = "identityCiphers";
- cipher = await this.cipherService.getNextIdentityCipher();
+ cipher = await this.cipherService.getNextIdentityCipher(activeUserId);
}
if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) {
diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts
index 7729981740d..d2b51c7ef40 100644
--- a/apps/browser/src/background/main.background.ts
+++ b/apps/browser/src/background/main.background.ts
@@ -1142,7 +1142,6 @@ export default class MainBackground {
this.accountService,
lockService,
this.billingAccountProfileStateService,
- this.cipherService,
);
this.nativeMessagingBackground = new NativeMessagingBackground(
this.keyService,
@@ -1259,6 +1258,7 @@ export default class MainBackground {
this.mainContextMenuHandler,
this.authService,
this.cipherService,
+ this.accountService,
);
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
@@ -1266,6 +1266,7 @@ export default class MainBackground {
this.platformUtilsService,
this.cipherService,
this.authService,
+ this.accountService,
chrome.webRequest,
);
}
@@ -1637,6 +1638,7 @@ export default class MainBackground {
this.i18nService,
this.platformUtilsService,
this.themeStateService,
+ this.accountService,
);
} else {
this.overlayBackground = new OverlayBackground(
@@ -1654,6 +1656,7 @@ export default class MainBackground {
this.inlineMenuFieldQualificationService,
this.themeStateService,
this.totpService,
+ this.accountService,
() => this.generatePassword(),
(password) => this.addPasswordToHistory(password),
);
diff --git a/apps/browser/src/background/nativeMessaging.background.ts b/apps/browser/src/background/nativeMessaging.background.ts
index 8c369e966c5..8100ff3cffa 100644
--- a/apps/browser/src/background/nativeMessaging.background.ts
+++ b/apps/browser/src/background/nativeMessaging.background.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { delay, filter, firstValueFrom, from, map, race, timer } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -57,26 +55,29 @@ type ReceiveMessageOuter = {
messageId?: number;
// Should only have one of these.
- message?: EncString;
+ message?: ReceiveMessage | EncString;
sharedSecret?: string;
};
type Callback = {
- resolver: any;
- rejecter: any;
+ resolver: (value?: unknown) => void;
+ rejecter: (reason?: any) => void;
+};
+
+type SecureChannel = {
+ privateKey: Uint8Array;
+ publicKey: Uint8Array;
+ sharedSecret?: SymmetricCryptoKey;
+ setupResolve: (value?: unknown) => void;
};
export class NativeMessagingBackground {
connected = false;
- private connecting: boolean;
- private port: browser.runtime.Port | chrome.runtime.Port;
+ private connecting: boolean = false;
+ private port?: browser.runtime.Port | chrome.runtime.Port;
+ private appId?: string;
- private privateKey: Uint8Array = null;
- private publicKey: Uint8Array = null;
- private secureSetupResolve: any = null;
- private sharedSecret: SymmetricCryptoKey;
- private appId: string;
- private validatingFingerprint: boolean;
+ private secureChannel?: SecureChannel;
private messageId = 0;
private callbacks = new Map();
@@ -108,11 +109,13 @@ export class NativeMessagingBackground {
async connect() {
this.logService.info("[Native Messaging IPC] Connecting to Bitwarden Desktop app...");
- this.appId = await this.appIdService.getAppId();
+ const appId = await this.appIdService.getAppId();
+ this.appId = appId;
await this.biometricStateService.setFingerprintValidated(false);
return new Promise((resolve, reject) => {
- this.port = BrowserApi.connectNative("com.8bit.bitwarden");
+ const port = BrowserApi.connectNative("com.8bit.bitwarden");
+ this.port = port;
this.connecting = true;
@@ -131,7 +134,8 @@ export class NativeMessagingBackground {
connectedCallback();
}
- this.port.onMessage.addListener(async (message: ReceiveMessageOuter) => {
+ port.onMessage.addListener(async (messageRaw: unknown) => {
+ const message = messageRaw as ReceiveMessageOuter;
switch (message.command) {
case "connected":
connectedCallback();
@@ -142,7 +146,7 @@ export class NativeMessagingBackground {
reject(new Error("startDesktop"));
}
this.connected = false;
- this.port.disconnect();
+ port.disconnect();
// reject all
for (const callback of this.callbacks.values()) {
callback.rejecter("disconnected");
@@ -151,18 +155,31 @@ export class NativeMessagingBackground {
break;
case "setupEncryption": {
// Ignore since it belongs to another device
- if (message.appId !== this.appId) {
+ if (message.appId !== appId) {
+ return;
+ }
+
+ if (message.sharedSecret == null) {
+ this.logService.info(
+ "[Native Messaging IPC] Unable to create secureChannel channel, no shared secret",
+ );
+ return;
+ }
+ if (this.secureChannel == null) {
+ this.logService.info(
+ "[Native Messaging IPC] Unable to create secureChannel channel, no secureChannel communication setup",
+ );
return;
}
const encrypted = Utils.fromB64ToArray(message.sharedSecret);
const decrypted = await this.cryptoFunctionService.rsaDecrypt(
encrypted,
- this.privateKey,
+ this.secureChannel.privateKey,
HashAlgorithmForEncryption,
);
- this.sharedSecret = new SymmetricCryptoKey(decrypted);
+ this.secureChannel.sharedSecret = new SymmetricCryptoKey(decrypted);
this.logService.info("[Native Messaging IPC] Secure channel established");
if ("messageId" in message) {
@@ -173,26 +190,27 @@ export class NativeMessagingBackground {
this.isConnectedToOutdatedDesktopClient = true;
}
- this.secureSetupResolve();
+ this.secureChannel.setupResolve();
break;
}
case "invalidateEncryption":
// Ignore since it belongs to another device
- if (message.appId !== this.appId) {
+ if (message.appId !== appId) {
return;
}
this.logService.warning(
"[Native Messaging IPC] Secure channel encountered an error; disconnecting and wiping keys...",
);
- this.sharedSecret = null;
- this.privateKey = null;
+ this.secureChannel = undefined;
this.connected = false;
- if (this.callbacks.has(message.messageId)) {
- this.callbacks.get(message.messageId).rejecter({
- message: "invalidateEncryption",
- });
+ if (message.messageId != null) {
+ if (this.callbacks.has(message.messageId)) {
+ this.callbacks.get(message.messageId)?.rejecter({
+ message: "invalidateEncryption",
+ });
+ }
}
return;
case "verifyFingerprint": {
@@ -217,21 +235,25 @@ export class NativeMessagingBackground {
break;
}
case "wrongUserId":
- if (this.callbacks.has(message.messageId)) {
- this.callbacks.get(message.messageId).rejecter({
- message: "wrongUserId",
- });
+ if (message.messageId != null) {
+ if (this.callbacks.has(message.messageId)) {
+ this.callbacks.get(message.messageId)?.rejecter({
+ message: "wrongUserId",
+ });
+ }
}
return;
default:
// Ignore since it belongs to another device
- if (!this.platformUtilsService.isSafari() && message.appId !== this.appId) {
+ if (!this.platformUtilsService.isSafari() && message.appId !== appId) {
return;
}
- // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- this.onMessage(message.message);
+ if (message.message != null) {
+ // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ this.onMessage(message.message);
+ }
}
});
@@ -240,16 +262,15 @@ export class NativeMessagingBackground {
if (BrowserApi.isWebExtensionsApi) {
error = p.error.message;
} else {
- error = chrome.runtime.lastError.message;
+ error = chrome.runtime.lastError?.message;
}
- this.sharedSecret = null;
- this.privateKey = null;
+ this.secureChannel = undefined;
this.connected = false;
this.logService.error("NativeMessaging port disconnected because of error: " + error);
- const reason = error != null ? "desktopIntegrationDisabled" : null;
+ const reason = error != null ? "desktopIntegrationDisabled" : undefined;
reject(new Error(reason));
});
});
@@ -293,13 +314,13 @@ export class NativeMessagingBackground {
);
const callback = this.callbacks.get(messageId);
this.callbacks.delete(messageId);
- callback.rejecter("errorConnecting");
+ callback?.rejecter("errorConnecting");
}
setTimeout(() => {
if (this.callbacks.has(messageId)) {
this.logService.info("[Native Messaging IPC] Message timed out and received no response");
- this.callbacks.get(messageId).rejecter({
+ this.callbacks.get(messageId)!.rejecter({
message: "timeout",
});
this.callbacks.delete(messageId);
@@ -320,16 +341,19 @@ export class NativeMessagingBackground {
if (this.platformUtilsService.isSafari()) {
this.postMessage(message as any);
} else {
- this.postMessage({ appId: this.appId, message: await this.encryptMessage(message) });
+ this.postMessage({ appId: this.appId!, message: await this.encryptMessage(message) });
}
}
async encryptMessage(message: Message) {
- if (this.sharedSecret == null) {
+ if (this.secureChannel?.sharedSecret == null) {
await this.secureCommunication();
}
- return await this.encryptService.encrypt(JSON.stringify(message), this.sharedSecret);
+ return await this.encryptService.encrypt(
+ JSON.stringify(message),
+ this.secureChannel!.sharedSecret!,
+ );
}
private postMessage(message: OuterMessage, messageId?: number) {
@@ -346,7 +370,7 @@ export class NativeMessagingBackground {
mac: message.message.mac,
};
}
- this.port.postMessage(msg);
+ this.port!.postMessage(msg);
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
@@ -354,26 +378,30 @@ export class NativeMessagingBackground {
"[Native Messaging IPC] Disconnected from Bitwarden Desktop app because of the native port disconnecting.",
);
- this.sharedSecret = null;
- this.privateKey = null;
+ this.secureChannel = undefined;
this.connected = false;
- if (this.callbacks.has(messageId)) {
- this.callbacks.get(messageId).rejecter("invalidateEncryption");
+ if (messageId != null && this.callbacks.has(messageId)) {
+ this.callbacks.get(messageId)!.rejecter("invalidateEncryption");
}
}
}
private async onMessage(rawMessage: ReceiveMessage | EncString) {
- let message = rawMessage as ReceiveMessage;
+ let message: ReceiveMessage;
if (!this.platformUtilsService.isSafari()) {
+ if (this.secureChannel?.sharedSecret == null) {
+ return;
+ }
message = JSON.parse(
await this.encryptService.decryptToUtf8(
rawMessage as EncString,
- this.sharedSecret,
+ this.secureChannel.sharedSecret,
"ipc-desktop-ipc-channel-key",
),
);
+ } else {
+ message = rawMessage as ReceiveMessage;
}
if (Math.abs(message.timestamp - Date.now()) > MessageValidTimeout) {
@@ -390,15 +418,17 @@ export class NativeMessagingBackground {
this.logService.info(
`[Native Messaging IPC] Received legacy message of type ${message.command}`,
);
- const messageId = this.callbacks.keys().next().value;
- const resolver = this.callbacks.get(messageId);
- this.callbacks.delete(messageId);
- resolver.resolver(message);
+ const messageId: number | undefined = this.callbacks.keys().next().value;
+ if (messageId != null) {
+ const resolver = this.callbacks.get(messageId);
+ this.callbacks.delete(messageId);
+ resolver!.resolver(message);
+ }
return;
}
if (this.callbacks.has(messageId)) {
- this.callbacks.get(messageId).resolver(message);
+ this.callbacks.get(messageId)!.resolver(message);
} else {
this.logService.info("[Native Messaging IPC] Received message without a callback", message);
}
@@ -406,8 +436,6 @@ export class NativeMessagingBackground {
private async secureCommunication() {
const [publicKey, privateKey] = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
- this.publicKey = publicKey;
- this.privateKey = privateKey;
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -419,7 +447,13 @@ export class NativeMessagingBackground {
messageId: this.messageId++,
});
- return new Promise((resolve, reject) => (this.secureSetupResolve = resolve));
+ return new Promise((resolve) => {
+ this.secureChannel = {
+ publicKey,
+ privateKey,
+ setupResolve: resolve,
+ };
+ });
}
private async sendUnencrypted(message: Message) {
@@ -429,11 +463,17 @@ export class NativeMessagingBackground {
message.timestamp = Date.now();
- this.postMessage({ appId: this.appId, message: message });
+ this.postMessage({ appId: this.appId!, message: message });
}
private async showFingerprintDialog() {
- const fingerprint = await this.keyService.getFingerprint(this.appId, this.publicKey);
+ if (this.secureChannel?.publicKey == null) {
+ return;
+ }
+ const fingerprint = await this.keyService.getFingerprint(
+ this.appId!,
+ this.secureChannel.publicKey,
+ );
this.messagingService.send("showNativeMessagingFingerprintDialog", {
fingerprint: fingerprint,
diff --git a/apps/browser/src/background/runtime.background.ts b/apps/browser/src/background/runtime.background.ts
index b9622e82005..2a756293070 100644
--- a/apps/browser/src/background/runtime.background.ts
+++ b/apps/browser/src/background/runtime.background.ts
@@ -16,7 +16,6 @@ import { MessageListener, isExternalMessage } from "@bitwarden/common/platform/m
import { devFlagEnabled } from "@bitwarden/common/platform/misc/flags";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { NotificationsService } from "@bitwarden/common/platform/notifications";
-import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { BiometricsCommands } from "@bitwarden/key-management";
@@ -54,7 +53,6 @@ export default class RuntimeBackground {
private accountService: AccountService,
private readonly lockService: LockService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
- private cipherService: CipherService,
) {
// onInstalled listener must be wired up before anything else, so we do it in the ctor
chrome.runtime.onInstalled.addListener((details: any) => {
@@ -202,9 +200,6 @@ export default class RuntimeBackground {
case BiometricsCommands.GetBiometricsStatusForUser: {
return await this.main.biometricsService.getBiometricsStatusForUser(msg.userId);
}
- case "updateLastUsedDate": {
- return await this.cipherService.updateLastUsedDate(msg.cipherId);
- }
case "getUseTreeWalkerApiForPageDetailsCollectionFeatureFlag": {
return await this.configService.getFeatureFlag(
FeatureFlag.UseTreeWalkerApiForPageDetailsCollection,
diff --git a/apps/browser/src/manifest.json b/apps/browser/src/manifest.json
index 6a2e017d06c..e5d5db20758 100644
--- a/apps/browser/src/manifest.json
+++ b/apps/browser/src/manifest.json
@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2025.1.3",
+ "version": "2025.2.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
diff --git a/apps/browser/src/manifest.v3.json b/apps/browser/src/manifest.v3.json
index dee6ebef31c..fe3aad37cf4 100644
--- a/apps/browser/src/manifest.v3.json
+++ b/apps/browser/src/manifest.v3.json
@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "__MSG_appName__",
- "version": "2025.1.3",
+ "version": "2025.2.1",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",
@@ -63,6 +63,22 @@
"webRequestAuthProvider",
"notifications"
],
+ "__firefox__permissions": [
+ "activeTab",
+ "alarms",
+ "clipboardRead",
+ "clipboardWrite",
+ "contextMenus",
+ "idle",
+ "scripting",
+ "storage",
+ "tabs",
+ "unlimitedStorage",
+ "webNavigation",
+ "webRequest",
+ "webRequestAuthProvider",
+ "notifications"
+ ],
"__safari__permissions": [
"activeTab",
"alarms",
@@ -86,7 +102,7 @@
"host_permissions": ["https://*/*", "http://*/*"],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
- "sandbox": "sandbox allow-scripts; script-src 'self'"
+ "__chrome__sandbox": "sandbox allow-scripts; script-src 'self'"
},
"sandbox": {
"pages": [
@@ -112,6 +128,13 @@
},
"description": "__MSG_commandOpenSidebar__"
},
+ "__opera___execute_sidebar_action": {
+ "suggested_key": {
+ "default": "Alt+Shift+Y",
+ "linux": "Alt+Shift+U"
+ },
+ "description": "__MSG_commandOpenSidebar__"
+ },
"autofill_login": {
"suggested_key": {
"default": "Ctrl+Shift+L"
@@ -166,5 +189,12 @@
"storage": {
"managed_schema": "managed_schema.json"
},
- "__firefox__storage": null
+ "__firefox__storage": null,
+ "__opera__sidebar_action": {
+ "default_title": "Bitwarden",
+ "default_panel": "popup/index.html?uilocation=sidebar",
+ "default_icon": "images/icon19.png",
+ "open_at_install": false,
+ "browser_style": false
+ }
}
diff --git a/apps/browser/src/platform/browser/browser-api.ts b/apps/browser/src/platform/browser/browser-api.ts
index 1d8ff65c17d..293fca4d029 100644
--- a/apps/browser/src/platform/browser/browser-api.ts
+++ b/apps/browser/src/platform/browser/browser-api.ts
@@ -530,10 +530,15 @@ export class BrowserApi {
win: Window & typeof globalThis,
): OperaSidebarAction | FirefoxSidebarAction | null {
const deviceType = BrowserPlatformUtilsService.getDevice(win);
- if (deviceType !== DeviceType.FirefoxExtension && deviceType !== DeviceType.OperaExtension) {
- return null;
+ if (deviceType === DeviceType.FirefoxExtension) {
+ return browser.sidebarAction;
}
- return win.opr?.sidebarAction || browser.sidebarAction;
+
+ if (deviceType === DeviceType.OperaExtension) {
+ return win.opr?.sidebarAction;
+ }
+
+ return null;
}
static captureVisibleTab(): Promise {
diff --git a/apps/browser/src/platform/listeners/update-badge.ts b/apps/browser/src/platform/listeners/update-badge.ts
index f67b96c847f..cd74b9ebf7d 100644
--- a/apps/browser/src/platform/listeners/update-badge.ts
+++ b/apps/browser/src/platform/listeners/update-badge.ts
@@ -2,8 +2,10 @@
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
+import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -22,6 +24,7 @@ export class UpdateBadge {
private authService: AuthService;
private badgeSettingsService: BadgeSettingsServiceAbstraction;
private cipherService: CipherService;
+ private accountService: AccountService;
private badgeAction: typeof chrome.action | typeof chrome.browserAction;
private sidebarAction: OperaSidebarAction | FirefoxSidebarAction;
private win: Window & typeof globalThis;
@@ -34,6 +37,7 @@ export class UpdateBadge {
this.badgeSettingsService = services.badgeSettingsService;
this.authService = services.authService;
this.cipherService = services.cipherService;
+ this.accountService = services.accountService;
}
async run(opts?: { tabId?: number; windowId?: number }): Promise {
@@ -87,7 +91,14 @@ export class UpdateBadge {
return;
}
- const ciphers = await this.cipherService.getAllDecryptedForUrl(opts?.tab?.url);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getOptionalUserId),
+ );
+ if (!activeUserId) {
+ return;
+ }
+
+ const ciphers = await this.cipherService.getAllDecryptedForUrl(opts?.tab?.url, activeUserId);
let countText = ciphers.length == 0 ? "" : ciphers.length.toString();
if (ciphers.length > 9) {
countText = "9+";
@@ -165,6 +176,13 @@ export class UpdateBadge {
return;
}
+ if ("opr" in this.win && BrowserApi.isManifestVersion(3)) {
+ // setIcon API is currenly broken for Opera MV3 extensions
+ // https://forums.opera.com/topic/75680/opr-sidebaraction-seticon-api-is-broken-access-to-extension-api-denied?_=1738349261570
+ // The API currently crashes on MacOS
+ return;
+ }
+
if (this.isOperaSidebar(this.sidebarAction)) {
await new Promise((resolve) =>
(this.sidebarAction as OperaSidebarAction).setIcon(options, () => resolve()),
diff --git a/apps/browser/src/platform/popup/layout/popup-layout.mdx b/apps/browser/src/platform/popup/layout/popup-layout.mdx
index 5723bef44b1..b93c649d695 100644
--- a/apps/browser/src/platform/popup/layout/popup-layout.mdx
+++ b/apps/browser/src/platform/popup/layout/popup-layout.mdx
@@ -100,9 +100,7 @@ Usage example:
### Transparent header
-
+
Common interactive elements to insert into the `end` slot are:
@@ -113,9 +111,7 @@ Common interactive elements to insert into the `end` slot are:
### Notice
-
+
Common interactive elements to insert into the `full-width-notice` slot are:
@@ -160,29 +156,21 @@ View the story source code to see examples of how to construct these types of pa
Example of wrapping an extension page in the `popup-tab-navigation` component.
-
+
## Extension Page
Examples of using just the `popup-page` component, without and with a footer.
-
+
-
+
## Popped out
When the browser extension is popped out, the "popout" button should not be passed to the header.
-
+
# Other stories
@@ -190,14 +178,10 @@ When the browser extension is popped out, the "popout" button should not be pass
An example of how to center the default content.
-
+
## Loading
An example of what the loading state looks like.
-
+
diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts
index 70a78ce548f..4dca29ee914 100644
--- a/apps/browser/src/popup/app-routing.module.ts
+++ b/apps/browser/src/popup/app-routing.module.ts
@@ -10,9 +10,9 @@ import {
import { unauthUiRefreshRedirect } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-redirect";
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
import {
+ activeAuthGuard,
authGuard,
lockGuard,
- activeAuthGuard,
redirectGuard,
tdeDecryptionRequiredGuard,
unauthGuardFn,
@@ -23,10 +23,14 @@ import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guard
import {
AnonLayoutWrapperComponent,
AnonLayoutWrapperData,
- LoginComponent,
- LoginSecondaryContentComponent,
+ DevicesIcon,
+ DeviceVerificationIcon,
LockIcon,
+ LoginComponent,
+ LoginDecryptionOptionsComponent,
+ LoginSecondaryContentComponent,
LoginViaAuthRequestComponent,
+ NewDeviceVerificationComponent,
PasswordHintComponent,
RegistrationFinishComponent,
RegistrationLockAltIcon,
@@ -35,14 +39,10 @@ import {
RegistrationStartSecondaryComponentData,
RegistrationUserAddIcon,
SetPasswordJitComponent,
- UserLockIcon,
- VaultIcon,
- LoginDecryptionOptionsComponent,
- DevicesIcon,
SsoComponent,
TwoFactorTimeoutIcon,
- NewDeviceVerificationComponent,
- DeviceVerificationIcon,
+ UserLockIcon,
+ VaultIcon,
} from "@bitwarden/auth/angular";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { LockComponent } from "@bitwarden/key-management-ui";
@@ -64,7 +64,6 @@ import { HomeComponent } from "../auth/popup/home.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
-import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
@@ -91,7 +90,9 @@ import { MoreFromBitwardenPageV2Component } from "../tools/popup/settings/about-
import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-browser-v2.component";
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
+import { canAccessAtRiskPasswords } from "../vault/guards/at-risk-passwords.guard";
import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard";
+import { AtRiskPasswordsComponent } from "../vault/popup/components/at-risk-passwords/at-risk-passwords.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
@@ -267,12 +268,6 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
- {
- path: "register",
- component: RegisterComponent,
- canActivate: [unauthGuardFn(unauthRouteOverrides)],
- data: { elevation: 1 } satisfies RouteDataProperties,
- },
{
path: "environment",
component: EnvironmentComponent,
@@ -759,6 +754,11 @@ const routes: Routes = [
},
],
},
+ {
+ path: "at-risk-passwords",
+ component: AtRiskPasswordsComponent,
+ canActivate: [authGuard, canAccessAtRiskPasswords],
+ },
{
path: "account-switcher",
component: AccountSwitcherComponent,
diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts
index 8147524d2f3..a97d001c54b 100644
--- a/apps/browser/src/popup/app.component.ts
+++ b/apps/browser/src/popup/app.component.ts
@@ -179,7 +179,7 @@ export class AppComponent implements OnInit, OnDestroy {
await this.clearComponentStates();
}
if (url.startsWith("/tabs/")) {
- await this.cipherService.setAddEditCipherInfo(null);
+ await this.cipherService.setAddEditCipherInfo(null, this.activeUserId);
}
(window as any).previousPopupUrl = url;
diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts
index 15a898aef53..3046ec7916a 100644
--- a/apps/browser/src/popup/app.module.ts
+++ b/apps/browser/src/popup/app.module.ts
@@ -26,7 +26,6 @@ import { HomeComponent } from "../auth/popup/home.component";
import { LoginDecryptionOptionsComponentV1 } from "../auth/popup/login-decryption-options/login-decryption-options-v1.component";
import { LoginComponentV1 } from "../auth/popup/login-v1.component";
import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-request-v1.component";
-import { RegisterComponent } from "../auth/popup/register.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
@@ -103,7 +102,6 @@ import "../platform/popup/locales";
LoginViaAuthRequestComponentV1,
LoginComponentV1,
LoginDecryptionOptionsComponentV1,
- RegisterComponent,
SetPasswordComponent,
SsoComponentV1,
TabsV2Component,
diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts
index 257497fb13d..210a05d9947 100644
--- a/apps/browser/src/popup/services/services.module.ts
+++ b/apps/browser/src/popup/services/services.module.ts
@@ -2,7 +2,7 @@
// @ts-strict-ignore
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
import { Router } from "@angular/router";
-import { Subject, merge, of } from "rxjs";
+import { merge, of, Subject } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
@@ -11,21 +11,21 @@ import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/sa
import {
CLIENT_TYPE,
DEFAULT_VAULT_TIMEOUT,
+ ENV_ADDITIONAL_REGIONS,
INTRAPROCESS_MESSAGING_SUBJECT,
MEMORY_STORAGE,
OBSERVABLE_DISK_STORAGE,
OBSERVABLE_MEMORY_STORAGE,
+ SafeInjectionToken,
SECURE_STORAGE,
SYSTEM_THEME_OBSERVABLE,
- SafeInjectionToken,
- ENV_ADDITIONAL_REGIONS,
} from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import {
AnonLayoutWrapperDataService,
LoginComponentService,
- SsoComponentService,
LoginDecryptionOptionsService,
+ SsoComponentService,
} from "@bitwarden/auth/angular";
import { LockService, LoginEmailService, PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -111,10 +111,10 @@ import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { CompactModeService, DialogService, ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import {
- KdfConfigService,
- KeyService,
BiometricsService,
DefaultKeyService,
+ KdfConfigService,
+ KeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { PasswordRepromptService } from "@bitwarden/vault";
diff --git a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts
index 86131176a6e..851509ab17f 100644
--- a/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts
+++ b/apps/browser/src/tools/popup/settings/export/export-browser-v2.component.ts
@@ -34,6 +34,6 @@ export class ExportBrowserV2Component {
constructor(private router: Router) {}
protected async onSuccessfulExport(organizationId: string): Promise {
- await this.router.navigate(["/vault-settings"]);
+ await this.router.navigate(["/tabs/settings"]);
}
}
diff --git a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts
index 66cb5c62f48..1c5558bd90e 100644
--- a/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts
+++ b/apps/browser/src/tools/popup/settings/import/import-browser-v2.component.ts
@@ -4,7 +4,7 @@ import { Router } from "@angular/router";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
-import { ImportComponent } from "@bitwarden/importer/ui";
+import { ImportComponent } from "@bitwarden/importer-ui";
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
diff --git a/apps/browser/src/vault/guards/at-risk-passwords.guard.ts b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts
new file mode 100644
index 00000000000..ee991c81239
--- /dev/null
+++ b/apps/browser/src/vault/guards/at-risk-passwords.guard.ts
@@ -0,0 +1,29 @@
+import { inject } from "@angular/core";
+import { CanActivateFn } from "@angular/router";
+import { switchMap, tap } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { ToastService } from "@bitwarden/components";
+import { filterOutNullish, TaskService } from "@bitwarden/vault";
+
+export const canAccessAtRiskPasswords: CanActivateFn = () => {
+ const accountService = inject(AccountService);
+ const taskService = inject(TaskService);
+ const toastService = inject(ToastService);
+ const i18nService = inject(I18nService);
+
+ return accountService.activeAccount$.pipe(
+ filterOutNullish(),
+ switchMap((user) => taskService.tasksEnabled$(user.id)),
+ tap((tasksEnabled) => {
+ if (!tasksEnabled) {
+ toastService.showToast({
+ variant: "error",
+ title: "",
+ message: i18nService.t("accessDenied"),
+ });
+ }
+ }),
+ );
+};
diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html
new file mode 100644
index 00000000000..16d9b6a322a
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.html
@@ -0,0 +1,9 @@
+
+
+
+ {{
+ (taskCount === 1 ? "reviewAndChangeAtRiskPassword" : "reviewAndChangeAtRiskPasswordsPlural")
+ | i18n: taskCount.toString()
+ }}
+
+
diff --git a/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts
new file mode 100644
index 00000000000..5e46f3cd3d9
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-callout/at-risk-password-callout.component.ts
@@ -0,0 +1,32 @@
+import { CommonModule } from "@angular/common";
+import { Component, inject } from "@angular/core";
+import { RouterModule } from "@angular/router";
+import { map, switchMap } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AnchorLinkDirective, CalloutModule } from "@bitwarden/components";
+import { I18nPipe } from "@bitwarden/ui-common";
+import { filterOutNullish, SecurityTaskType, TaskService } from "@bitwarden/vault";
+
+// TODO: This component will need to be reworked to use the new EndUserNotificationService in PM-10609
+
+@Component({
+ selector: "vault-at-risk-password-callout",
+ standalone: true,
+ imports: [CommonModule, AnchorLinkDirective, RouterModule, CalloutModule, I18nPipe],
+ templateUrl: "./at-risk-password-callout.component.html",
+})
+export class AtRiskPasswordCalloutComponent {
+ private taskService = inject(TaskService);
+ private activeAccount$ = inject(AccountService).activeAccount$.pipe(filterOutNullish());
+
+ protected pendingTasks$ = this.activeAccount$.pipe(
+ switchMap((user) =>
+ this.taskService
+ .pendingTasks$(user.id)
+ .pipe(
+ map((tasks) => tasks.filter((t) => t.type === SecurityTaskType.UpdateAtRiskCredential)),
+ ),
+ ),
+ );
+}
diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts
new file mode 100644
index 00000000000..f8cd4a60650
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-password-page.service.ts
@@ -0,0 +1,35 @@
+import { inject, Injectable } from "@angular/core";
+import { map, Observable } from "rxjs";
+
+import {
+ BANNERS_DISMISSED_DISK,
+ StateProvider,
+ UserKeyDefinition,
+} from "@bitwarden/common/platform/state";
+import { UserId } from "@bitwarden/common/types/guid";
+
+export const AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY = new UserKeyDefinition(
+ BANNERS_DISMISSED_DISK,
+ "atRiskPasswordAutofillBannerDismissed",
+ {
+ deserializer: (bannersDismissed) => bannersDismissed,
+ clearOn: [], // Do not clear dismissed banners
+ },
+);
+
+@Injectable()
+export class AtRiskPasswordPageService {
+ private stateProvider = inject(StateProvider);
+
+ isCalloutDismissed(userId: UserId): Observable {
+ return this.stateProvider
+ .getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY)
+ .state$.pipe(map((dismissed) => !!dismissed));
+ }
+
+ async dismissCallout(userId: UserId): Promise {
+ await this.stateProvider
+ .getUser(userId, AT_RISK_PASSWORD_AUTOFILL_CALLOUT_DISMISSED_KEY)
+ .update(() => true);
+ }
+}
diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.html b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.html
new file mode 100644
index 00000000000..ece00af3df2
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.html
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+ {{ "changeAtRiskPasswordsFasterDesc" | i18n }}
+
+
+
+
+
+ {{ pageDescription$ | async }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts
new file mode 100644
index 00000000000..c71c9fa56c0
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.spec.ts
@@ -0,0 +1,265 @@
+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 { BehaviorSubject, firstValueFrom, of } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { IconComponent } from "@bitwarden/angular/vault/components/icon.component";
+import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
+import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
+import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import { ToastService } from "@bitwarden/components";
+import {
+ PasswordRepromptService,
+ SecurityTask,
+ SecurityTaskType,
+ TaskService,
+} from "@bitwarden/vault";
+
+import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
+
+import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
+import { AtRiskPasswordsComponent } from "./at-risk-passwords.component";
+
+@Component({
+ standalone: true,
+ selector: "popup-header",
+ template: ``,
+})
+class MockPopupHeaderComponent {
+ @Input() pageTitle: string | undefined;
+ @Input() backAction: (() => void) | undefined;
+}
+
+@Component({
+ standalone: true,
+ selector: "popup-page",
+ template: ``,
+})
+class MockPopupPageComponent {
+ @Input() loading: boolean | undefined;
+}
+
+@Component({
+ standalone: true,
+ selector: "app-vault-icon",
+ template: ``,
+})
+class MockAppIcon {
+ @Input() cipher: CipherView | undefined;
+}
+
+describe("AtRiskPasswordsComponent", () => {
+ let component: AtRiskPasswordsComponent;
+ let fixture: ComponentFixture;
+
+ let mockTasks$: BehaviorSubject;
+ let mockCiphers$: BehaviorSubject;
+ let mockOrgs$: BehaviorSubject;
+ let mockInlineMenuVisibility$: BehaviorSubject;
+ let calloutDismissed$: BehaviorSubject;
+ const setInlineMenuVisibility = jest.fn();
+ const mockToastService = mock();
+ const mockAtRiskPasswordPageService = mock();
+
+ beforeEach(async () => {
+ mockTasks$ = new BehaviorSubject([
+ {
+ id: "task",
+ organizationId: "org",
+ cipherId: "cipher",
+ type: SecurityTaskType.UpdateAtRiskCredential,
+ } as SecurityTask,
+ ]);
+ mockCiphers$ = new BehaviorSubject([
+ {
+ id: "cipher",
+ organizationId: "org",
+ name: "Item 1",
+ } as CipherView,
+ {
+ id: "cipher2",
+ organizationId: "org",
+ name: "Item 2",
+ } as CipherView,
+ ]);
+ mockOrgs$ = new BehaviorSubject([
+ {
+ id: "org",
+ name: "Org 1",
+ } as Organization,
+ ]);
+
+ mockInlineMenuVisibility$ = new BehaviorSubject(
+ AutofillOverlayVisibility.Off,
+ );
+
+ calloutDismissed$ = new BehaviorSubject(false);
+ setInlineMenuVisibility.mockClear();
+ mockToastService.showToast.mockClear();
+ mockAtRiskPasswordPageService.isCalloutDismissed.mockReturnValue(calloutDismissed$);
+
+ await TestBed.configureTestingModule({
+ imports: [AtRiskPasswordsComponent],
+ providers: [
+ {
+ provide: TaskService,
+ useValue: {
+ pendingTasks$: () => mockTasks$,
+ },
+ },
+ {
+ provide: OrganizationService,
+ useValue: {
+ organizations$: () => mockOrgs$,
+ },
+ },
+ {
+ provide: CipherService,
+ useValue: {
+ cipherViews$: () => mockCiphers$,
+ },
+ },
+ { provide: I18nService, useValue: { t: (key: string) => key } },
+ { provide: AccountService, useValue: { activeAccount$: of({ id: "user" }) } },
+ { provide: PlatformUtilsService, useValue: mock() },
+ { provide: PasswordRepromptService, useValue: mock() },
+ {
+ provide: AutofillSettingsServiceAbstraction,
+ useValue: {
+ inlineMenuVisibility$: mockInlineMenuVisibility$,
+ setInlineMenuVisibility: setInlineMenuVisibility,
+ },
+ },
+ { provide: ToastService, useValue: mockToastService },
+ ],
+ })
+ .overrideModule(JslibModule, {
+ remove: {
+ imports: [IconComponent],
+ exports: [IconComponent],
+ },
+ add: {
+ imports: [MockAppIcon],
+ exports: [MockAppIcon],
+ },
+ })
+ .overrideComponent(AtRiskPasswordsComponent, {
+ remove: {
+ imports: [PopupHeaderComponent, PopupPageComponent],
+ providers: [AtRiskPasswordPageService],
+ },
+ add: {
+ imports: [MockPopupHeaderComponent, MockPopupPageComponent],
+ providers: [
+ { provide: AtRiskPasswordPageService, useValue: mockAtRiskPasswordPageService },
+ ],
+ },
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(AtRiskPasswordsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe("pending atRiskItems$", () => {
+ it("should list pending at risk item tasks", async () => {
+ const items = await firstValueFrom(component["atRiskItems$"]);
+ expect(items).toHaveLength(1);
+ expect(items[0].name).toBe("Item 1");
+ });
+ });
+
+ describe("pageDescription$", () => {
+ it("should use single org description when tasks belong to one org", async () => {
+ const description = await firstValueFrom(component["pageDescription$"]);
+ expect(description).toBe("atRiskPasswordsDescSingleOrg");
+ });
+
+ it("should use multiple org description when tasks belong to multiple orgs", async () => {
+ mockTasks$.next([
+ {
+ id: "task",
+ organizationId: "org",
+ cipherId: "cipher",
+ type: SecurityTaskType.UpdateAtRiskCredential,
+ } as SecurityTask,
+ {
+ id: "task2",
+ organizationId: "org2",
+ cipherId: "cipher2",
+ type: SecurityTaskType.UpdateAtRiskCredential,
+ } as SecurityTask,
+ ]);
+ const description = await firstValueFrom(component["pageDescription$"]);
+ expect(description).toBe("atRiskPasswordsDescMultiOrg");
+ });
+ });
+
+ describe("autofill callout", () => {
+ it("should show the callout if inline autofill is disabled", async () => {
+ mockInlineMenuVisibility$.next(AutofillOverlayVisibility.Off);
+ calloutDismissed$.next(false);
+ fixture.detectChanges();
+ const callout = fixture.debugElement.query(By.css('[data-testid="autofill-callout"]'));
+
+ expect(callout).toBeTruthy();
+ });
+
+ it("should hide the callout if inline autofill is enabled", async () => {
+ mockInlineMenuVisibility$.next(AutofillOverlayVisibility.OnButtonClick);
+ calloutDismissed$.next(false);
+ fixture.detectChanges();
+ const callout = fixture.debugElement.query(By.css('[data-testid="autofill-callout"]'));
+
+ expect(callout).toBeFalsy();
+ });
+
+ it("should hide the callout if the user has previously dismissed it", async () => {
+ mockInlineMenuVisibility$.next(AutofillOverlayVisibility.Off);
+ calloutDismissed$.next(true);
+ fixture.detectChanges();
+ const callout = fixture.debugElement.query(By.css('[data-testid="autofill-callout"]'));
+
+ expect(callout).toBeFalsy();
+ });
+
+ it("should call dismissCallout when the dismiss callout button is clicked", async () => {
+ mockInlineMenuVisibility$.next(AutofillOverlayVisibility.Off);
+ fixture.detectChanges();
+ const dismissButton = fixture.debugElement.query(
+ By.css('[data-testid="dismiss-callout-button"]'),
+ );
+ dismissButton.nativeElement.click();
+ expect(mockAtRiskPasswordPageService.dismissCallout).toHaveBeenCalled();
+ });
+
+ describe("turn on autofill button", () => {
+ it("should call the service to turn on inline autofill and show a toast", () => {
+ const button = fixture.debugElement.query(
+ By.css('[data-testid="turn-on-autofill-button"]'),
+ );
+ button.nativeElement.click();
+
+ expect(setInlineMenuVisibility).toHaveBeenCalledWith(
+ AutofillOverlayVisibility.OnButtonClick,
+ );
+ expect(mockToastService.showToast).toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts
new file mode 100644
index 00000000000..f075335102f
--- /dev/null
+++ b/apps/browser/src/vault/popup/components/at-risk-passwords/at-risk-passwords.component.ts
@@ -0,0 +1,162 @@
+import { CommonModule } from "@angular/common";
+import { Component, inject } from "@angular/core";
+import { Router } from "@angular/router";
+import { combineLatest, firstValueFrom, map, of, shareReplay, startWith, switchMap } from "rxjs";
+
+import { JslibModule } from "@bitwarden/angular/jslib.module";
+import {
+ getOrganizationById,
+ OrganizationService,
+} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
+import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
+import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
+import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
+import {
+ BadgeComponent,
+ ButtonModule,
+ CalloutModule,
+ ItemModule,
+ ToastService,
+ TypographyModule,
+} from "@bitwarden/components";
+import {
+ filterOutNullish,
+ PasswordRepromptService,
+ SecurityTaskType,
+ TaskService,
+} from "@bitwarden/vault";
+
+import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
+import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
+import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
+
+import { AtRiskPasswordPageService } from "./at-risk-password-page.service";
+
+@Component({
+ selector: "vault-at-risk-passwords",
+ standalone: true,
+ imports: [
+ PopupPageComponent,
+ PopupHeaderComponent,
+ PopOutComponent,
+ ItemModule,
+ CommonModule,
+ JslibModule,
+ BadgeComponent,
+ TypographyModule,
+ CalloutModule,
+ ButtonModule,
+ ],
+ providers: [AtRiskPasswordPageService],
+ templateUrl: "./at-risk-passwords.component.html",
+})
+export class AtRiskPasswordsComponent {
+ private taskService = inject(TaskService);
+ private organizationService = inject(OrganizationService);
+ private cipherService = inject(CipherService);
+ private i18nService = inject(I18nService);
+ private accountService = inject(AccountService);
+ private platformUtilsService = inject(PlatformUtilsService);
+ private passwordRepromptService = inject(PasswordRepromptService);
+ private router = inject(Router);
+ private autofillSettingsService = inject(AutofillSettingsServiceAbstraction);
+ private toastService = inject(ToastService);
+ private atRiskPasswordPageService = inject(AtRiskPasswordPageService);
+
+ private activeUserData$ = this.accountService.activeAccount$.pipe(
+ filterOutNullish(),
+ switchMap((user) =>
+ combineLatest([
+ this.taskService.pendingTasks$(user.id),
+ this.cipherService.cipherViews$(user.id).pipe(
+ filterOutNullish(),
+ map((ciphers) => Object.fromEntries(ciphers.map((c) => [c.id, c]))),
+ ),
+ of(user),
+ ]),
+ ),
+ map(([tasks, ciphers, user]) => ({
+ tasks,
+ ciphers,
+ userId: user.id,
+ })),
+ shareReplay({ bufferSize: 1, refCount: true }),
+ );
+
+ protected loading$ = this.activeUserData$.pipe(
+ map(() => false),
+ startWith(true),
+ );
+
+ protected calloutDismissed$ = this.activeUserData$.pipe(
+ switchMap(({ userId }) => this.atRiskPasswordPageService.isCalloutDismissed(userId)),
+ );
+
+ protected inlineAutofillSettingEnabled$ = this.autofillSettingsService.inlineMenuVisibility$.pipe(
+ map((setting) => setting !== AutofillOverlayVisibility.Off),
+ );
+
+ protected atRiskItems$ = this.activeUserData$.pipe(
+ map(({ tasks, ciphers }) =>
+ tasks
+ .filter(
+ (t) =>
+ t.type === SecurityTaskType.UpdateAtRiskCredential &&
+ t.cipherId != null &&
+ ciphers[t.cipherId] != null,
+ )
+ .map((t) => ciphers[t.cipherId!]),
+ ),
+ );
+
+ protected pageDescription$ = this.activeUserData$.pipe(
+ switchMap(({ tasks, userId }) => {
+ const orgIds = new Set(tasks.map((t) => t.organizationId));
+ if (orgIds.size === 1) {
+ const [orgId] = orgIds;
+ return this.organizationService.organizations$(userId).pipe(
+ getOrganizationById(orgId),
+ map((org) => this.i18nService.t("atRiskPasswordsDescSingleOrg", org?.name, tasks.length)),
+ );
+ }
+
+ return of(this.i18nService.t("atRiskPasswordsDescMultiOrg", tasks.length));
+ }),
+ );
+
+ async viewCipher(cipher: CipherView) {
+ const repromptPassed = await this.passwordRepromptService.passwordRepromptCheck(cipher);
+ if (!repromptPassed) {
+ return;
+ }
+ await this.router.navigate(["/view-cipher"], {
+ queryParams: { cipherId: cipher.id, type: cipher.type },
+ });
+ }
+
+ async launchChangePassword(cipher: CipherView) {
+ if (cipher.login?.uri) {
+ this.platformUtilsService.launchUri(cipher.login.uri);
+ }
+ }
+
+ async activateInlineAutofillMenuVisibility() {
+ await this.autofillSettingsService.setInlineMenuVisibility(
+ AutofillOverlayVisibility.OnButtonClick,
+ );
+ this.toastService.showToast({
+ variant: "success",
+ message: this.i18nService.t("turnedOnAutofill"),
+ title: "",
+ });
+ }
+
+ async dismissCallout() {
+ const { userId } = await firstValueFrom(this.activeUserData$);
+ await this.atRiskPasswordPageService.dismissCallout(userId);
+ }
+}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts
index 3252f030fc3..6974e6f7359 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts
@@ -1,14 +1,17 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended";
-import { BehaviorSubject, Observable } from "rxjs";
+import { BehaviorSubject } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
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 { mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -58,9 +61,9 @@ describe("AddEditV2Component", () => {
collect.mockClear();
addEditCipherInfo$ = new BehaviorSubject(null);
- cipherServiceMock = mock();
- cipherServiceMock.addEditCipherInfo$ =
- addEditCipherInfo$.asObservable() as Observable;
+ cipherServiceMock = mock({
+ addEditCipherInfo$: jest.fn().mockReturnValue(addEditCipherInfo$),
+ });
await TestBed.configureTestingModule({
imports: [AddEditV2Component],
@@ -81,6 +84,7 @@ describe("AddEditV2Component", () => {
canDeleteCipher$: jest.fn().mockReturnValue(true),
},
},
+ { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
],
})
.overrideProvider(CipherFormConfigService, {
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 b46b1d61509..1dcb48c918d 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
@@ -9,10 +9,12 @@ import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
+import { CipherId, CollectionId, OrganizationId, 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";
@@ -180,6 +182,7 @@ export class AddEditV2Component implements OnInit {
private toastService: ToastService,
private dialogService: DialogService,
protected cipherAuthorizationService: CipherAuthorizationService,
+ private accountService: AccountService,
) {
this.subscribeToParams();
}
@@ -281,9 +284,15 @@ export class AddEditV2Component implements OnInit {
config.initialValues = this.setInitialValuesFromParams(params);
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getUserId),
+ );
+
// The browser notification bar and overlay use addEditCipherInfo$ to pass modified cipher details to the form
// Attempt to fetch them here and overwrite the initialValues if present
- const cachedCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$);
+ const cachedCipherInfo = await firstValueFrom(
+ this.cipherService.addEditCipherInfo$(activeUserId),
+ );
if (cachedCipherInfo != null) {
// Cached cipher info has priority over queryParams
@@ -292,7 +301,7 @@ export class AddEditV2Component implements OnInit {
...mapAddEditCipherInfoToInitialValues(cachedCipherInfo),
};
// Be sure to clear the "cached" cipher info, so it doesn't get used again
- await this.cipherService.setAddEditCipherInfo(null);
+ await this.cipherService.setAddEditCipherInfo(null, activeUserId);
}
if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) {
@@ -371,7 +380,8 @@ export class AddEditV2Component implements OnInit {
}
try {
- await this.deleteCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.deleteCipher(activeUserId);
} catch (e) {
this.logService.error(e);
return false;
@@ -388,10 +398,10 @@ export class AddEditV2Component implements OnInit {
return true;
};
- protected deleteCipher() {
+ protected deleteCipher(userId: UserId) {
return this.config.originalCipher.deletedDate
- ? this.cipherService.deleteWithServer(this.config.originalCipher.id)
- : this.cipherService.softDeleteWithServer(this.config.originalCipher.id);
+ ? this.cipherService.deleteWithServer(this.config.originalCipher.id, userId)
+ : this.cipherService.softDeleteWithServer(this.config.originalCipher.id, userId);
}
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts
index 51ebe9bdb62..27f3b7e5e18 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts
@@ -5,12 +5,13 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ReactiveFormsModule } from "@angular/forms";
import { ActivatedRoute } from "@angular/router";
-import { Observable, combineLatest, first, map, switchMap } from "rxjs";
+import { Observable, combineLatest, filter, first, map, switchMap } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationId } from "@bitwarden/common/types/guid";
+import { OrgKey, UserKey } from "@bitwarden/common/types/key";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -58,16 +59,19 @@ export class AssignCollections {
private accountService: AccountService,
route: ActivatedRoute,
) {
- const cipher$: Observable = route.queryParams.pipe(
- switchMap(({ cipherId }) => this.cipherService.get(cipherId)),
- switchMap((cipherDomain) =>
- this.accountService.activeAccount$.pipe(
- map((account) => account?.id),
- switchMap((userId) =>
- this.cipherService
- .getKeyForCipherKeyDecryption(cipherDomain, userId)
- .then(cipherDomain.decrypt.bind(cipherDomain)),
- ),
+ const cipher$: Observable = this.accountService.activeAccount$.pipe(
+ map((account) => account?.id),
+ filter((userId) => userId != null),
+ switchMap((userId) =>
+ route.queryParams.pipe(
+ switchMap(async ({ cipherId }) => {
+ const cipherDomain = await this.cipherService.get(cipherId, userId);
+ const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption(
+ cipherDomain,
+ userId,
+ );
+ return cipherDomain.decrypt(key);
+ }),
),
),
);
diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts
index aca494716b1..1bc7e22e6d5 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts
@@ -77,10 +77,10 @@ export class OpenAttachmentsComponent implements OnInit {
return;
}
- const cipherDomain = await this.cipherService.get(this.cipherId);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
+ const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
index 1f67dd51c21..fb5ac4b3391 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts
@@ -59,7 +59,9 @@ describe("VaultHeaderV2Component", () => {
providers: [
{
provide: CipherService,
- useValue: mock({ cipherViews$: new BehaviorSubject([]) }),
+ useValue: mock({
+ cipherViews$: jest.fn().mockReturnValue(new BehaviorSubject([])),
+ }),
},
{ provide: VaultSettingsService, useValue: mock() },
{ provide: FolderService, useValue: mock() },
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 f95790cda5f..cb758e7a48d 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
@@ -22,6 +22,8 @@ import { Router } from "@angular/router";
import { firstValueFrom, Observable, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId } from "@bitwarden/common/types/guid";
@@ -265,6 +267,7 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private router: Router,
private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService,
+ private accountService: AccountService,
) {}
ngOnInit(): void {
@@ -311,7 +314,8 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
this.viewCipherTimeout = null;
}
- await this.cipherService.updateLastLaunchedDate(cipher.id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.cipherService.updateLastLaunchedDate(cipher.id, activeUserId);
await BrowserApi.createNewTab(cipher.login.launchUri);
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts
index 9ac17b49386..838ce2e9426 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts
@@ -1,13 +1,15 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended";
-import { BehaviorSubject, Subject } from "rxjs";
+import { Subject } from "rxjs";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
-import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.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";
+import { mockAccountServiceWith } from "@bitwarden/common/spec";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -19,6 +21,7 @@ import { PasswordHistoryV2Component } from "./vault-password-history-v2.componen
describe("PasswordHistoryV2Component", () => {
let fixture: ComponentFixture;
const params$ = new Subject();
+ const mockUserId = "acct-1" as UserId;
const mockCipherView = {
id: "111-222-333",
@@ -45,9 +48,7 @@ describe("PasswordHistoryV2Component", () => {
{ provide: CipherService, useValue: mock({ get: getCipher }) },
{
provide: AccountService,
- useValue: mock({
- activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account),
- }),
+ useValue: mockAccountServiceWith(mockUserId),
},
{ provide: PopupRouterCacheService, useValue: { back } },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } },
@@ -64,7 +65,7 @@ describe("PasswordHistoryV2Component", () => {
tick(100);
- expect(getCipher).toHaveBeenCalledWith(mockCipherView.id);
+ expect(getCipher).toHaveBeenCalledWith(mockCipherView.id, mockUserId);
}));
it("navigates back when a cipherId is not in the params", () => {
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts
index e20bb1f1bcd..5d315775b10 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts
@@ -58,8 +58,6 @@ export class PasswordHistoryV2Component implements OnInit {
/** Load the cipher based on the given Id */
private async loadCipher(cipherId: string) {
- const cipher = await this.cipherService.get(cipherId);
-
const activeAccount = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)),
);
@@ -69,6 +67,8 @@ export class PasswordHistoryV2Component implements OnInit {
}
const activeUserId = activeAccount.id as UserId;
+
+ const cipher = await this.cipherService.get(cipherId, activeUserId);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
index 6e017042711..8cb538a429a 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.html
@@ -32,6 +32,9 @@
slot="above-scroll-area"
*ngIf="vaultState !== VaultStateEnum.Empty && !(loading$ | async)"
>
+
diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
index 12952a69c79..be5c33aab70 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts
@@ -3,13 +3,31 @@ import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterLink } from "@angular/router";
-import { combineLatest, filter, map, Observable, shareReplay, switchMap, take } from "rxjs";
+import {
+ combineLatest,
+ filter,
+ map,
+ firstValueFrom,
+ Observable,
+ shareReplay,
+ switchMap,
+ take,
+} from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
-import { ButtonModule, DialogService, Icons, NoItemsModule } from "@bitwarden/components";
+import {
+ BannerComponent,
+ ButtonModule,
+ DialogService,
+ Icons,
+ NoItemsModule,
+} from "@bitwarden/components";
import { DecryptionFailureDialogComponent, VaultIcons } from "@bitwarden/vault";
import { CurrentAccountComponent } from "../../../../auth/popup/account-switching/current-account.component";
@@ -19,6 +37,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
import { VaultPopupScrollPositionService } from "../../services/vault-popup-scroll-position.service";
+import { AtRiskPasswordCalloutComponent } from "../at-risk-callout/at-risk-password-callout.component";
import { BlockedInjectionBanner } from "./blocked-injection-banner/blocked-injection-banner.component";
import {
@@ -56,6 +75,8 @@ enum VaultState {
ScrollingModule,
VaultHeaderV2Component,
DecryptionFailureDialogComponent,
+ BannerComponent,
+ AtRiskPasswordCalloutComponent,
],
})
export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
@@ -96,6 +117,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupListFiltersService: VaultPopupListFiltersService,
private vaultScrollPositionService: VaultPopupScrollPositionService,
+ private accountService: AccountService,
private destroyRef: DestroyRef,
private cipherService: CipherService,
private dialogService: DialogService,
@@ -136,7 +158,10 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
}
async ngOnInit() {
- this.cipherService.failedToDecryptCiphers$
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ this.cipherService
+ .failedToDecryptCiphers$(activeUserId)
.pipe(
map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
filter((ciphers) => ciphers.length > 0),
@@ -153,4 +178,6 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
ngOnDestroy(): void {
this.vaultScrollPositionService.stop();
}
+
+ protected readonly FeatureFlag = FeatureFlag;
}
diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
index 39feb86f4fd..17464c025af 100644
--- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
+++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts
@@ -150,7 +150,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises
- expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444");
+ expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId);
expect(component.cipher).toEqual(mockCipher);
}));
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
index 65fb024ee99..209691869f0 100644
--- 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
@@ -5,13 +5,14 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
-import { firstValueFrom, map, Observable, switchMap } from "rxjs";
+import { firstValueFrom, Observable, switchMap } from "rxjs";
import { CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import {
AUTOFILL_ID,
COPY_PASSWORD_ID,
@@ -22,6 +23,7 @@ import {
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
@@ -87,6 +89,8 @@ type LoadAction =
],
})
export class ViewV2Component {
+ private activeUserId: UserId;
+
headerText: string;
cipher: CipherView;
organization$: Observable;
@@ -117,14 +121,20 @@ export class ViewV2Component {
subscribeToParams(): void {
this.route.queryParams
.pipe(
- switchMap(async (params): Promise => {
+ switchMap(async (params) => {
this.loadAction = params.action;
this.senderTabId = params.senderTabId ? parseInt(params.senderTabId, 10) : undefined;
- return await this.getCipherData(params.cipherId);
+
+ const activeUserId = await firstValueFrom(
+ this.accountService.activeAccount$.pipe(getUserId),
+ );
+ const cipher = await this.getCipherData(params.cipherId, activeUserId);
+ return { activeUserId, cipher };
}),
- switchMap(async (cipher) => {
+ switchMap(async ({ activeUserId, cipher }) => {
this.cipher = cipher;
this.headerText = this.setHeader(cipher.type);
+ this.activeUserId = activeUserId;
if (this.loadAction) {
await this._handleLoadAction(this.loadAction, this.senderTabId);
@@ -159,13 +169,10 @@ export class ViewV2Component {
}
}
- async getCipherData(id: string) {
- const cipher = await this.cipherService.get(id);
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ async getCipherData(id: string, userId: UserId) {
+ const cipher = await this.cipherService.get(id, userId);
return await cipher.decrypt(
- await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
+ await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
);
}
@@ -213,7 +220,7 @@ export class ViewV2Component {
restore = async (): Promise => {
try {
- await this.cipherService.restoreWithServer(this.cipher.id);
+ await this.cipherService.restoreWithServer(this.cipher.id, this.activeUserId);
} catch (e) {
this.logService.error(e);
}
@@ -228,8 +235,8 @@ export class ViewV2Component {
protected deleteCipher() {
return this.cipher.isDeleted
- ? this.cipherService.deleteWithServer(this.cipher.id)
- : this.cipherService.softDeleteWithServer(this.cipher.id);
+ ? this.cipherService.deleteWithServer(this.cipher.id, this.activeUserId)
+ : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId);
}
protected showFooter(): boolean {
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 ec20458ca60..6d7b7b57d23 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
@@ -15,6 +15,8 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
+import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
+import { LocalData } from "@bitwarden/common/vault/models/data/local.data";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
@@ -34,6 +36,10 @@ describe("VaultPopupItemsService", () => {
let mockCollections: CollectionView[];
let activeUserLastSync$: BehaviorSubject;
+ let ciphersSubject: BehaviorSubject>;
+ let localDataSubject: BehaviorSubject>;
+ let failedToDecryptCiphersSubject: BehaviorSubject;
+
const cipherServiceMock = mock();
const vaultSettingsServiceMock = mock();
const organizationServiceMock = mock();
@@ -60,9 +66,21 @@ describe("VaultPopupItemsService", () => {
cipherList[3].favorite = true;
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
- cipherServiceMock.ciphers$ = new BehaviorSubject(null);
- cipherServiceMock.localData$ = new BehaviorSubject(null);
- cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]);
+
+ ciphersSubject = new BehaviorSubject>({});
+ localDataSubject = new BehaviorSubject>({});
+ failedToDecryptCiphersSubject = new BehaviorSubject([]);
+
+ cipherServiceMock.ciphers$.mockImplementation((userId: UserId) =>
+ ciphersSubject.asObservable(),
+ );
+ cipherServiceMock.localData$.mockImplementation((userId: UserId) =>
+ localDataSubject.asObservable(),
+ );
+ cipherServiceMock.failedToDecryptCiphers$.mockImplementation((userId: UserId) =>
+ failedToDecryptCiphersSubject.asObservable(),
+ );
+
searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
ciphers.filter((c) => ["0", "1"].includes(c.id)),
@@ -118,6 +136,7 @@ describe("VaultPopupItemsService", () => {
{ provide: CollectionService, useValue: collectionService },
{ provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock },
{ provide: SyncService, useValue: syncServiceMock },
+ { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
{
provide: InlineMenuFieldQualificationService,
useValue: inlineMenuFieldQualificationServiceMock,
@@ -155,7 +174,7 @@ describe("VaultPopupItemsService", () => {
await tracker.expectEmission();
- (cipherServiceMock.ciphers$ as BehaviorSubject).next(null);
+ ciphersSubject.next({});
await tracker.expectEmission();
@@ -169,7 +188,7 @@ describe("VaultPopupItemsService", () => {
await tracker.expectEmission();
- (cipherServiceMock.localData$ as BehaviorSubject).next(null);
+ localDataSubject.next({});
await tracker.expectEmission();
@@ -373,7 +392,7 @@ describe("VaultPopupItemsService", () => {
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
- (cipherServiceMock.ciphers$ as BehaviorSubject).next(null);
+ ciphersSubject.next({});
const deletedCiphers = await firstValueFrom(service.deletedCiphers$);
expect(deletedCiphers.length).toBe(1);
@@ -422,7 +441,7 @@ describe("VaultPopupItemsService", () => {
it("should cycle when cipherService.ciphers$ emits", async () => {
// Restart tracking
tracked = new ObservableTracker(service.loading$);
- (cipherServiceMock.ciphers$ as BehaviorSubject).next(null);
+ ciphersSubject.next({});
await trackedCiphers.pauseUntilReceived(2);
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 8e0711abb1e..0b3e7eba492 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
@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { inject, Injectable, NgZone } from "@angular/core";
+import { Injectable, NgZone } from "@angular/core";
import {
BehaviorSubject,
combineLatest,
@@ -86,16 +86,19 @@ export class VaultPopupItemsService {
* Observable that contains the list of all decrypted ciphers.
* @private
*/
- private _allDecryptedCiphers$: Observable = merge(
- this.cipherService.ciphers$,
- this.cipherService.localData$,
- ).pipe(
- runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular
- tap(() => this._ciphersLoading$.next()),
- waitUntilSync(this.syncService),
- switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())),
- withLatestFrom(this.cipherService.failedToDecryptCiphers$),
- map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
+ private _allDecryptedCiphers$: Observable = this.accountService.activeAccount$.pipe(
+ map((a) => a?.id),
+ filter((userId) => userId != null),
+ switchMap((userId) =>
+ merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
+ runInsideAngular(this.ngZone),
+ tap(() => this._ciphersLoading$.next()),
+ waitUntilSync(this.syncService),
+ switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted(userId))),
+ withLatestFrom(this.cipherService.failedToDecryptCiphers$(userId)),
+ map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
+ ),
+ ),
shareReplay({ refCount: true, bufferSize: 1 }),
);
@@ -281,6 +284,7 @@ export class VaultPopupItemsService {
private vaultPopupAutofillService: VaultPopupAutofillService,
private syncService: SyncService,
private accountService: AccountService,
+ private ngZone: NgZone,
) {}
applyFilter(newSearchText: string) {
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
index 7f570e8f5c9..ec823d5738f 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts
@@ -41,7 +41,7 @@ describe("VaultPopupListFiltersService", () => {
} as unknown as FolderService;
const cipherService = {
- cipherViews$,
+ cipherViews$: () => cipherViews$,
} as unknown as CipherService;
const organizationService = {
diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
index 579319c92ab..b1451318499 100644
--- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
+++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts
@@ -93,16 +93,6 @@ export class VaultPopupListFiltersService {
*/
private cipherViews: CipherView[] = [];
- /**
- * Observable of cipher views
- */
- private cipherViews$: Observable = this.cipherService.cipherViews$.pipe(
- tap((cipherViews) => {
- this.cipherViews = Object.values(cipherViews);
- }),
- map((ciphers) => Object.values(ciphers)),
- );
-
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
@@ -271,8 +261,16 @@ export class VaultPopupListFiltersService {
* Folder array structured to be directly passed to `ChipSelectComponent`
*/
folders$: Observable[]> = this.activeUserId$.pipe(
- switchMap((userId) =>
- combineLatest([
+ switchMap((userId) => {
+ // Observable of cipher views
+ const cipherViews$ = this.cipherService.cipherViews$(userId).pipe(
+ tap((cipherViews) => {
+ this.cipherViews = Object.values(cipherViews);
+ }),
+ map((ciphers) => Object.values(ciphers)),
+ );
+
+ return combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
@@ -281,7 +279,7 @@ export class VaultPopupListFiltersService {
),
),
this.folderService.folderViews$(userId),
- this.cipherViews$,
+ cipherViews$,
]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) {
@@ -330,8 +328,8 @@ export class VaultPopupListFiltersService {
map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
),
- ),
- ),
+ );
+ }),
);
/**
diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
index c56d1c7d10d..7e30ab26035 100644
--- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
+++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts
@@ -3,8 +3,11 @@
import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
import { Router } from "@angular/router";
+import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
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";
@@ -65,6 +68,7 @@ export class TrashListItemsContainerComponent {
private i18nService: I18nService,
private dialogService: DialogService,
private passwordRepromptService: PasswordRepromptService,
+ private accountService: AccountService,
private router: Router,
) {}
@@ -81,7 +85,8 @@ export class TrashListItemsContainerComponent {
async restore(cipher: CipherView) {
try {
- await this.cipherService.restoreWithServer(cipher.id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.cipherService.restoreWithServer(cipher.id, activeUserId);
await this.router.navigate(["/trash"]);
this.toastService.showToast({
@@ -112,7 +117,8 @@ export class TrashListItemsContainerComponent {
}
try {
- await this.cipherService.deleteWithServer(cipher.id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.cipherService.deleteWithServer(cipher.id, activeUserId);
await this.router.navigate(["/trash"]);
this.toastService.showToast({
diff --git a/apps/browser/store/locales/pt_PT/copy.resx b/apps/browser/store/locales/pt_PT/copy.resx
index 65b81e2d2d4..fa62a0f1d5c 100644
--- a/apps/browser/store/locales/pt_PT/copy.resx
+++ b/apps/browser/store/locales/pt_PT/copy.resx
@@ -175,7 +175,7 @@ As soluções de gestão de credenciais encriptadas ponto a ponto do Bitwarden p
Sincronize e aceda ao seu cofre através de vários dispositivos
- Gira todas as suas credenciais e palavras-passe a partir de um cofre seguro
+ Faça a gestão de todas as suas credenciais e palavras-passe a partir de um cofre seguro
Preencha rapidamente e de forma automática as suas credenciais de início de sessão em qualquer site que visite
diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json
index 81b9bc870c4..8055260db57 100644
--- a/apps/browser/tsconfig.json
+++ b/apps/browser/tsconfig.json
@@ -25,7 +25,7 @@
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
- "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
+ "@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
diff --git a/apps/browser/webpack.config.js b/apps/browser/webpack.config.js
index ba60a577a71..d077e54bb73 100644
--- a/apps/browser/webpack.config.js
+++ b/apps/browser/webpack.config.js
@@ -317,19 +317,24 @@ if (manifestVersion == 2) {
configs.push(mainConfig);
} else {
- // Manifest v3 needs an extra helper for utilities in the content script.
- // The javascript output of this should be added to manifest.v3.json
- mainConfig.entry["content/misc-utils"] = "./src/autofill/content/misc-utils.ts";
- mainConfig.entry["offscreen-document/offscreen-document"] =
- "./src/platform/offscreen-document/offscreen-document.ts";
+ // Firefox does not use the offscreen API
+ if (browser !== "firefox") {
+ // Manifest v3 needs an extra helper for utilities in the content script.
+ // The javascript output of this should be added to manifest.v3.json
+ mainConfig.entry["content/misc-utils"] = "./src/autofill/content/misc-utils.ts";
+ mainConfig.entry["offscreen-document/offscreen-document"] =
+ "./src/platform/offscreen-document/offscreen-document.ts";
- mainConfig.plugins.push(
- new HtmlWebpackPlugin({
- template: "./src/platform/offscreen-document/index.html",
- filename: "offscreen-document/index.html",
- chunks: ["offscreen-document/offscreen-document"],
- }),
- );
+ mainConfig.plugins.push(
+ new HtmlWebpackPlugin({
+ template: "./src/platform/offscreen-document/index.html",
+ filename: "offscreen-document/index.html",
+ chunks: ["offscreen-document/offscreen-document"],
+ }),
+ );
+ }
+
+ const target = browser === "firefox" ? "web" : "webworker";
/**
* @type {import("webpack").Configuration}
@@ -339,7 +344,7 @@ if (manifestVersion == 2) {
mode: ENV,
devtool: false,
entry: "./src/platform/background.ts",
- target: "webworker",
+ target: target,
output: {
filename: "background.js",
path: path.resolve(__dirname, "build"),
diff --git a/apps/cli/package.json b/apps/cli/package.json
index 391c9c80808..17dfca83dfa 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": "2025.1.3",
+ "version": "2025.2.0",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts
index e26d073326e..6d9e6c8b6c0 100644
--- a/apps/cli/src/admin-console/commands/share.command.ts
+++ b/apps/cli/src/admin-console/commands/share.command.ts
@@ -1,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Response } from "../../models/response";
@@ -48,7 +49,9 @@ export class ShareCommand {
organizationId = organizationId.toLowerCase();
}
- const cipher = await this.cipherService.get(id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -56,15 +59,12 @@ export class ShareCommand {
return Response.badRequest("This item already belongs to an organization.");
}
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
try {
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId);
- const updatedCipher = await this.cipherService.get(cipher.id);
+ const updatedCipher = await this.cipherService.get(cipher.id, activeUserId);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
);
diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts
index 9af28863c09..2d4a854135d 100644
--- a/apps/cli/src/commands/edit.command.ts
+++ b/apps/cli/src/commands/edit.command.ts
@@ -1,11 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { CollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.export";
@@ -25,8 +26,6 @@ import { CipherResponse } from "../vault/models/cipher.response";
import { FolderResponse } from "../vault/models/folder.response";
export class EditCommand {
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -85,14 +84,12 @@ export class EditCommand {
}
private async editCipher(id: string, req: CipherExport) {
- const cipher = await this.cipherService.get(id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
let cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
@@ -114,7 +111,9 @@ export class EditCommand {
}
private async editCipherCollections(id: string, req: string[]) {
- const cipher = await this.cipherService.get(id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -129,11 +128,14 @@ export class EditCommand {
cipher.collectionIds = req;
try {
- const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher);
+ const updatedCipher = await this.cipherService.saveCollectionsWithServer(
+ cipher,
+ activeUserId,
+ );
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher,
- await firstValueFrom(this.activeUserId$),
+ await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
),
);
const res = new CipherResponse(decCipher);
@@ -144,7 +146,7 @@ export class EditCommand {
}
private async editFolder(id: string, req: FolderExport) {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts
index a90bfa64894..92c3a8baeaf 100644
--- a/apps/cli/src/commands/get.command.ts
+++ b/apps/cli/src/commands/get.command.ts
@@ -52,8 +52,6 @@ import { FolderResponse } from "../vault/models/folder.response";
import { DownloadCommand } from "./download.command";
export class GetCommand extends DownloadCommand {
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -114,16 +112,16 @@ export class GetCommand extends DownloadCommand {
private async getCipherView(id: string): Promise {
let decCipher: CipherView = null;
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (Utils.isGuid(id)) {
- const cipher = await this.cipherService.get(id);
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher != null) {
- const activeUserId = await firstValueFrom(this.activeUserId$);
decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
}
} else if (id.trim() !== "") {
- let ciphers = await this.cipherService.getAllDecrypted();
+ let ciphers = await this.cipherService.getAllDecrypted(activeUserId);
ciphers = this.searchService.searchCiphersBasic(ciphers, id);
if (ciphers.length > 1) {
return ciphers;
@@ -265,8 +263,10 @@ export class GetCommand extends DownloadCommand {
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(account.id),
);
+
if (!canAccessPremium) {
- const originalCipher = await this.cipherService.get(cipher.id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const originalCipher = await this.cipherService.get(cipher.id, activeUserId);
if (
originalCipher == null ||
originalCipher.organizationId == null ||
@@ -352,7 +352,8 @@ export class GetCommand extends DownloadCommand {
this.accountProfileService.hasPremiumFromAnySource$(account.id),
);
if (!canAccessPremium) {
- const originalCipher = await this.cipherService.get(cipher.id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const originalCipher = await this.cipherService.get(cipher.id, activeUserId);
if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature.");
}
@@ -384,7 +385,7 @@ export class GetCommand extends DownloadCommand {
private async getFolder(id: string) {
let decFolder: FolderView = null;
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) {
@@ -561,7 +562,7 @@ export class GetCommand extends DownloadCommand {
private async getFingerprint(id: string) {
let fingerprint: string[] = null;
if (id === "me") {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
} else if (Utils.isGuid(id)) {
diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts
index 5e01af798a4..5d512d81bf5 100644
--- a/apps/cli/src/commands/list.command.ts
+++ b/apps/cli/src/commands/list.command.ts
@@ -65,11 +65,14 @@ export class ListCommand {
private async listCiphers(options: Options) {
let ciphers: CipherView[];
+
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") {
- ciphers = await this.cipherService.getAllDecryptedForUrl(options.url);
+ ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId);
} else {
- ciphers = await this.cipherService.getAllDecrypted();
+ ciphers = await this.cipherService.getAllDecrypted(activeUserId);
}
if (
@@ -138,9 +141,8 @@ export class ListCommand {
}
private async listFolders(options: Options) {
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
if (options.search != null && options.search.trim() !== "") {
diff --git a/apps/cli/src/commands/restore.command.ts b/apps/cli/src/commands/restore.command.ts
index 96179e7b687..fd2f6f239ef 100644
--- a/apps/cli/src/commands/restore.command.ts
+++ b/apps/cli/src/commands/restore.command.ts
@@ -1,9 +1,16 @@
+import { firstValueFrom } from "rxjs";
+
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Response } from "../models/response";
export class RestoreCommand {
- constructor(private cipherService: CipherService) {}
+ constructor(
+ private cipherService: CipherService,
+ private accountService: AccountService,
+ ) {}
async run(object: string, id: string): Promise {
if (id != null) {
@@ -19,7 +26,9 @@ export class RestoreCommand {
}
private async restoreCipher(id: string) {
- const cipher = await this.cipherService.get(id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -28,7 +37,7 @@ export class RestoreCommand {
}
try {
- await this.cipherService.restoreWithServer(id);
+ await this.cipherService.restoreWithServer(id, activeUserId);
return Response.success();
} catch (e) {
return Response.error(e);
diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts
index be476d19814..dec09447839 100644
--- a/apps/cli/src/oss-serve-configurator.ts
+++ b/apps/cli/src/oss-serve-configurator.ts
@@ -124,7 +124,10 @@ export class OssServeConfigurator {
this.serviceContainer.encryptService,
this.serviceContainer.organizationUserApiService,
);
- this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService);
+ this.restoreCommand = new RestoreCommand(
+ this.serviceContainer.cipherService,
+ this.serviceContainer.accountService,
+ );
this.shareCommand = new ShareCommand(
this.serviceContainer.cipherService,
this.serviceContainer.accountService,
diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts
index f3eb6eef613..d6ef27b1428 100644
--- a/apps/cli/src/vault.program.ts
+++ b/apps/cli/src/vault.program.ts
@@ -347,7 +347,10 @@ export class VaultProgram extends BaseProgram {
}
await this.exitIfLocked();
- const command = new RestoreCommand(this.serviceContainer.cipherService);
+ const command = new RestoreCommand(
+ this.serviceContainer.cipherService,
+ this.serviceContainer.accountService,
+ );
const response = await command.run(object, id);
this.processResponse(response);
});
diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts
index 28f58187fdb..713471356c9 100644
--- a/apps/cli/src/vault/create.command.ts
+++ b/apps/cli/src/vault/create.command.ts
@@ -10,6 +10,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
@@ -30,8 +31,6 @@ import { CipherResponse } from "./models/cipher.response";
import { FolderResponse } from "./models/folder.response";
export class CreateCommand {
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
constructor(
private cipherService: CipherService,
private folderService: FolderService,
@@ -90,7 +89,7 @@ export class CreateCommand {
}
private async createCipher(req: CipherExport) {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try {
const newCipher = await this.cipherService.createWithServer(cipher);
@@ -132,14 +131,14 @@ export class CreateCommand {
return Response.badRequest("File name not provided.");
}
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
const itemId = options.itemId.toLowerCase();
- const cipher = await this.cipherService.get(itemId);
+ const cipher = await this.cipherService.get(itemId, activeUserId);
if (cipher == null) {
return Response.notFound();
}
- const activeUserId = await firstValueFrom(this.activeUserId$);
-
const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
);
@@ -173,7 +172,7 @@ export class CreateCommand {
}
private async createFolder(req: FolderExport) {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try {
diff --git a/apps/cli/src/vault/delete.command.ts b/apps/cli/src/vault/delete.command.ts
index a285f8f5b34..9e648cd9bb0 100644
--- a/apps/cli/src/vault/delete.command.ts
+++ b/apps/cli/src/vault/delete.command.ts
@@ -1,7 +1,8 @@
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -44,7 +45,9 @@ export class DeleteCommand {
}
private async deleteCipher(id: string, options: Options) {
- const cipher = await this.cipherService.get(id);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ const cipher = await this.cipherService.get(id, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -59,9 +62,9 @@ export class DeleteCommand {
try {
if (options.permanent) {
- await this.cipherService.deleteWithServer(id);
+ await this.cipherService.deleteWithServer(id, activeUserId);
} else {
- await this.cipherService.softDeleteWithServer(id);
+ await this.cipherService.softDeleteWithServer(id, activeUserId);
}
return Response.success();
} catch (e) {
@@ -74,8 +77,10 @@ export class DeleteCommand {
return Response.badRequest("`itemid` option is required.");
}
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
const itemId = options.itemId.toLowerCase();
- const cipher = await this.cipherService.get(itemId);
+ const cipher = await this.cipherService.get(itemId, activeUserId);
if (cipher == null) {
return Response.notFound();
}
@@ -89,16 +94,19 @@ export class DeleteCommand {
return Response.error("Attachment `" + id + "` was not found.");
}
- const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom(
- this.accountProfileService.hasPremiumFromAnySource$(account.id),
+ this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
);
if (cipher.organizationId == null && !canAccessPremium) {
return Response.error("Premium status is required to use this feature.");
}
try {
- await this.cipherService.deleteAttachmentWithServer(cipher.id, attachments[0].id);
+ await this.cipherService.deleteAttachmentWithServer(
+ cipher.id,
+ attachments[0].id,
+ activeUserId,
+ );
return Response.success();
} catch (e) {
return Response.error(e);
@@ -106,9 +114,7 @@ export class DeleteCommand {
}
private async deleteFolder(id: string) {
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
diff --git a/apps/desktop/package.json b/apps/desktop/package.json
index 6ab3459c7f8..014cc219464 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": "2025.1.4",
+ "version": "2025.2.1",
"keywords": [
"bitwarden",
"password",
diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html
index 709506f576f..a47f938efd2 100644
--- a/apps/desktop/src/app/accounts/settings.component.html
+++ b/apps/desktop/src/app/accounts/settings.component.html
@@ -436,6 +436,23 @@
"enableSshAgentDesc" | i18n
}}
+
diff --git a/apps/desktop/src/vault/app/vault/view.component.ts b/apps/desktop/src/vault/app/vault/view.component.ts
index 136a537dd37..aee1f34437b 100644
--- a/apps/desktop/src/vault/app/vault/view.component.ts
+++ b/apps/desktop/src/vault/app/vault/view.component.ts
@@ -159,10 +159,4 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
this.messagingService.send("premiumRequired");
}
}
-
- upgradeOrganization() {
- this.messagingService.send("upgradeOrganization", {
- organizationId: this.cipher.organizationId,
- });
- }
}
diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json
index 1bd97b0130f..0bef5a5564d 100644
--- a/apps/desktop/tsconfig.json
+++ b/apps/desktop/tsconfig.json
@@ -23,7 +23,7 @@
"@bitwarden/generator-legacy": ["../../libs/tools/generator/extensions/legacy/src"],
"@bitwarden/generator-navigation": ["../../libs/tools/generator/extensions/navigation/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
- "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
+ "@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/node/*": ["../../libs/node/src/*"],
diff --git a/apps/web/package.json b/apps/web/package.json
index 7047b0fb137..cf2df649773 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
- "version": "2025.1.2",
+ "version": "2025.2.1",
"scripts": {
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
diff --git a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts
index 58efc7348e1..68ba5830c35 100644
--- a/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts
+++ b/apps/web/src/app/admin-console/organizations/manage/entity-events.component.ts
@@ -1,8 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DIALOG_DATA, DialogConfig } from "@angular/cdk/dialog";
-import { Component, Inject, OnInit } from "@angular/core";
+import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
+import { ActivatedRoute, Router } from "@angular/router";
+import { firstValueFrom, switchMap } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
@@ -12,7 +14,6 @@ import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { EventView } from "@bitwarden/common/models/view/event.view";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
@@ -34,7 +35,7 @@ export interface EntityEventsDialogParams {
templateUrl: "entity-events.component.html",
standalone: true,
})
-export class EntityEventsComponent implements OnInit {
+export class EntityEventsComponent implements OnInit, OnDestroy {
loading = true;
continuationToken: string;
protected dataSource = new TableDataSource
();
@@ -59,13 +60,14 @@ export class EntityEventsComponent implements OnInit {
private apiService: ApiService,
private i18nService: I18nService,
private eventService: EventService,
- private platformUtilsService: PlatformUtilsService,
private userNamePipe: UserNamePipe,
private logService: LogService,
private organizationUserApiService: OrganizationUserApiService,
private formBuilder: FormBuilder,
private validationService: ValidationService,
private toastService: ToastService,
+ private router: Router,
+ private activeRoute: ActivatedRoute,
) {}
async ngOnInit() {
@@ -77,6 +79,21 @@ export class EntityEventsComponent implements OnInit {
await this.load();
}
+ async ngOnDestroy() {
+ await firstValueFrom(
+ this.activeRoute.queryParams.pipe(
+ switchMap(async (params) => {
+ await this.router.navigate([], {
+ queryParams: {
+ ...params,
+ viewEvents: null,
+ },
+ });
+ }),
+ ),
+ );
+ }
+
async load() {
try {
if (this.showUser) {
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html
index bb5294ebf02..f666decae81 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html
+++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-delete-dialog.component.html
@@ -7,9 +7,9 @@
{{ error }}
- 0 && !error">
- {{ "deleteManyOrganizationUsersWarningDesc" | i18n }}
-
+ 0 && !error">
+ {{ "deleteManyOrganizationUsersWarningDesc" | i18n }}
+
diff --git a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html
index ebe27afabf5..0c64a150425 100644
--- a/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html
+++ b/apps/web/src/app/admin-console/organizations/members/components/bulk/bulk-restore-revoke.component.html
@@ -3,9 +3,9 @@
*ngIf="{ enabled: accountDeprovisioningEnabled$ | async } as accountDeprovisioning"
>
- {{ bulkMemberTitle }}
+ {{ bulkMemberTitle }}
- {{ bulkTitle }}
+ {{ bulkTitle }}
@@ -46,10 +46,7 @@
- 0 && !error && isRevoking">
- {{ "revokeUsersWarning" | i18n }}
-
-
+ 0 && !error && isRevoking">{{ "revokeUsersWarning" | i18n }}
@@ -109,7 +106,13 @@
-
-
+
|
{
+ showOrgAtRiskMembers = async (invokerId: string) => {
const dialogData = this.reportService.generateAtRiskMemberList(this.dataSource.data);
- this.dataService.setDrawerForOrgAtRiskMembers(dialogData);
+ this.dataService.setDrawerForOrgAtRiskMembers(dialogData, invokerId);
};
- showOrgAtRiskApps = async () => {
+ showOrgAtRiskApps = async (invokerId: string) => {
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
- this.dataService.setDrawerForOrgAtRiskApps(data);
+ this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
};
onCheckboxChange(applicationName: string, event: Event) {
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html
index 72e60c470b0..4dc4b7ffb1a 100644
--- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html
+++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.html
@@ -35,19 +35,27 @@
@@ -70,7 +78,11 @@
|
-
+
|
|
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts
index 4d820a3cc66..f1fa38dd28f 100644
--- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts
+++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/critical-applications.component.ts
@@ -131,17 +131,17 @@ export class CriticalApplicationsComponent implements OnInit {
?.atRiskMemberDetails ?? [],
applicationName,
};
- this.dataService.setDrawerForAppAtRiskMembers(data);
+ this.dataService.setDrawerForAppAtRiskMembers(data, applicationName);
};
- showOrgAtRiskMembers = async () => {
+ showOrgAtRiskMembers = async (invokerId: string) => {
const data = this.reportService.generateAtRiskMemberList(this.dataSource.data);
- this.dataService.setDrawerForOrgAtRiskMembers(data);
+ this.dataService.setDrawerForOrgAtRiskMembers(data, invokerId);
};
- showOrgAtRiskApps = async () => {
+ showOrgAtRiskApps = async (invokerId: string) => {
const data = this.reportService.generateAtRiskApplicationList(this.dataSource.data);
- this.dataService.setDrawerForOrgAtRiskApps(data);
+ this.dataService.setDrawerForOrgAtRiskApps(data, invokerId);
};
trackByFunction(_: number, item: ApplicationHealthReportDetailWithCriticalFlag) {
diff --git a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
index a368f5c0c18..12082e888b0 100644
--- a/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
+++ b/bitwarden_license/bit-web/src/app/tools/access-intelligence/risk-insights.component.html
@@ -56,7 +56,11 @@
-
+
{
expect(apiService.send).toHaveBeenCalledWith(
"POST",
`/tasks/${organizationId}/bulk-create`,
- tasks,
+ { tasks },
true,
true,
);
diff --git a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts
index 442fde9dbf6..520fb744486 100644
--- a/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts
+++ b/bitwarden_license/bit-web/src/app/vault/services/default-admin-task.service.ts
@@ -43,6 +43,12 @@ export class DefaultAdminTaskService implements AdminTaskService {
organizationId: OrganizationId,
tasks: CreateTasksRequest[],
): Promise {
- await this.apiService.send("POST", `/tasks/${organizationId}/bulk-create`, tasks, true, true);
+ await this.apiService.send(
+ "POST",
+ `/tasks/${organizationId}/bulk-create`,
+ { tasks },
+ true,
+ true,
+ );
}
}
diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json
index 1c9a530d273..82e0b7f57fa 100644
--- a/bitwarden_license/bit-web/tsconfig.json
+++ b/bitwarden_license/bit-web/tsconfig.json
@@ -22,7 +22,7 @@
],
"@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"],
"@bitwarden/importer-core": ["../../libs/importer/src"],
- "@bitwarden/importer/ui": ["../../libs/importer/src/components"],
+ "@bitwarden/importer-ui": ["../../libs/importer/src/components"],
"@bitwarden/key-management": ["../../libs/key-management/src"],
"@bitwarden/key-management-ui": ["../../libs/key-management-ui/src"],
"@bitwarden/platform": ["../../libs/platform/src"],
diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts
index 52a22ac2946..5f39966468f 100644
--- a/libs/angular/src/admin-console/components/collections.component.ts
+++ b/libs/angular/src/admin-console/components/collections.component.ts
@@ -7,9 +7,11 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -45,11 +47,9 @@ export class CollectionsComponent implements OnInit {
}
async load() {
- this.cipherDomain = await this.loadCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ this.cipherDomain = await this.loadCipher(activeUserId);
this.collectionIds = this.loadCipherCollections();
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
@@ -95,7 +95,8 @@ export class CollectionsComponent implements OnInit {
}
this.cipherDomain.collectionIds = selectedCollectionIds;
try {
- this.formPromise = this.saveCollections();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ this.formPromise = this.saveCollections(activeUserId);
await this.formPromise;
this.onSavedCollections.emit();
this.toastService.showToast({
@@ -114,8 +115,8 @@ export class CollectionsComponent implements OnInit {
}
}
- protected loadCipher() {
- return this.cipherService.get(this.cipherId);
+ protected loadCipher(userId: UserId) {
+ return this.cipherService.get(this.cipherId, userId);
}
protected loadCipherCollections() {
@@ -129,7 +130,7 @@ export class CollectionsComponent implements OnInit {
);
}
- protected saveCollections() {
- return this.cipherService.saveCollectionsWithServer(this.cipherDomain);
+ protected saveCollections(userId: UserId) {
+ return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId);
}
}
diff --git a/libs/angular/src/auth/components/environment-selector.component.html b/libs/angular/src/auth/components/environment-selector.component.html
index 786afe40371..19f49f73dd2 100644
--- a/libs/angular/src/auth/components/environment-selector.component.html
+++ b/libs/angular/src/auth/components/environment-selector.component.html
@@ -3,7 +3,7 @@
selectedRegion: selectedRegion$ | async,
} as data"
>
-
+
{{ "accessing" | i18n }}:
-
+
{{ data.selectedRegion.domain }}
@@ -35,9 +35,9 @@
(backdropClick)="isOpen = false"
(detach)="close()"
>
-
+
();
-
- showPassword = false;
- formPromise: Promise;
- referenceData: ReferenceEventRequest;
- showTerms = true;
- showErrorSummary = false;
- passwordStrengthResult: any;
- characterMinimumMessage: string;
- minimumLength = Utils.minimumPasswordLength;
- color: string;
- text: string;
-
- formGroup = this.formBuilder.group(
- {
- email: ["", [Validators.required, Validators.email]],
- name: [""],
- masterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
- confirmMasterPassword: ["", [Validators.required, Validators.minLength(this.minimumLength)]],
- hint: [
- null,
- [
- InputsFieldMatch.validateInputsDoesntMatch(
- "masterPassword",
- this.i18nService.t("hintEqualsPassword"),
- ),
- ],
- ],
- checkForBreaches: [true],
- acceptPolicies: [false, [this.acceptPoliciesValidation()]],
- },
- {
- validator: InputsFieldMatch.validateFormInputsMatch(
- "masterPassword",
- "confirmMasterPassword",
- this.i18nService.t("masterPassDoesntMatch"),
- ),
- },
- );
-
- protected successRoute = "login";
-
- protected accountCreated = false;
-
- protected captchaBypassToken: string = null;
-
- // allows for extending classes to modify the register request before sending
- // currently used by web to add organization invitation details
- protected modifyRegisterRequest: (request: RegisterRequest) => Promise;
-
- constructor(
- protected formValidationErrorService: FormValidationErrorsService,
- protected formBuilder: UntypedFormBuilder,
- protected loginStrategyService: LoginStrategyServiceAbstraction,
- protected router: Router,
- i18nService: I18nService,
- protected keyService: KeyService,
- protected apiService: ApiService,
- platformUtilsService: PlatformUtilsService,
- environmentService: EnvironmentService,
- protected logService: LogService,
- protected auditService: AuditService,
- protected dialogService: DialogService,
- protected toastService: ToastService,
- ) {
- super(environmentService, i18nService, platformUtilsService, toastService);
- this.showTerms = !platformUtilsService.isSelfHost();
- this.characterMinimumMessage = this.i18nService.t("characterMinimum", this.minimumLength);
- }
-
- async ngOnInit() {
- // 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.setupCaptcha();
- }
-
- async submit(showToast = true) {
- let email = this.formGroup.value.email;
- email = email.trim().toLowerCase();
- let name = this.formGroup.value.name;
- name = name === "" ? null : name; // Why do we do this?
- const masterPassword = this.formGroup.value.masterPassword;
- try {
- if (!this.accountCreated) {
- const registerResponse = await this.registerAccount(
- await this.buildRegisterRequest(email, masterPassword, name),
- showToast,
- );
- if (!registerResponse.successful) {
- return;
- }
- this.captchaBypassToken = registerResponse.captchaBypassToken;
- this.accountCreated = true;
- }
- if (this.isInTrialFlow) {
- if (!this.accountCreated) {
- this.toastService.showToast({
- variant: "success",
- title: null,
- message: this.i18nService.t("trialAccountCreated"),
- });
- }
- const loginResponse = await this.logIn(email, masterPassword, this.captchaBypassToken);
- if (loginResponse.captchaRequired) {
- return;
- }
- this.createdAccount.emit(this.formGroup.value.email);
- } else {
- this.toastService.showToast({
- variant: "success",
- title: null,
- message: this.i18nService.t("newAccountCreated"),
- });
- // 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.successRoute], { queryParams: { email: email } });
- }
- } catch (e) {
- this.logService.error(e);
- }
- }
-
- togglePassword() {
- this.showPassword = !this.showPassword;
- }
-
- getStrengthResult(result: any) {
- this.passwordStrengthResult = result;
- }
-
- getPasswordScoreText(event: PasswordColorText) {
- this.color = event.color;
- this.text = event.text;
- }
-
- private getErrorToastMessage() {
- const error: AllValidationErrors = this.formValidationErrorService
- .getFormValidationErrors(this.formGroup.controls)
- .shift();
-
- if (error) {
- switch (error.errorName) {
- case "email":
- return this.i18nService.t("invalidEmail");
- case "inputsDoesntMatchError":
- return this.i18nService.t("masterPassDoesntMatch");
- case "inputsMatchError":
- return this.i18nService.t("hintEqualsPassword");
- case "minlength":
- return this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength);
- default:
- return this.i18nService.t(this.errorTag(error));
- }
- }
-
- return;
- }
-
- private errorTag(error: AllValidationErrors): string {
- const name = error.errorName.charAt(0).toUpperCase() + error.errorName.slice(1);
- return `${error.controlName}${name}`;
- }
-
- //validation would be ignored on selfhosted
- private acceptPoliciesValidation(): ValidatorFn {
- return (control: AbstractControl) => {
- const ctrlValue = control.value;
-
- return !ctrlValue && this.showTerms ? { required: true } : null;
- };
- }
-
- private async validateRegistration(showToast: boolean): Promise<{ isValid: boolean }> {
- this.formGroup.markAllAsTouched();
- this.showErrorSummary = true;
-
- if (this.formGroup.get("acceptPolicies").hasError("required")) {
- this.toastService.showToast({
- variant: "error",
- title: this.i18nService.t("errorOccurred"),
- message: this.i18nService.t("acceptPoliciesRequired"),
- });
- return { isValid: false };
- }
-
- //web
- if (this.formGroup.invalid && !showToast) {
- return { isValid: false };
- }
-
- //desktop, browser
- if (this.formGroup.invalid && showToast) {
- const errorText = this.getErrorToastMessage();
- this.toastService.showToast({
- variant: "error",
- title: this.i18nService.t("errorOccurred"),
- message: errorText,
- });
- return { isValid: false };
- }
-
- const passwordWeak =
- this.passwordStrengthResult != null && this.passwordStrengthResult.score < 3;
- const passwordLeak =
- this.formGroup.controls.checkForBreaches.value &&
- (await this.auditService.passwordLeaked(this.formGroup.controls.masterPassword.value)) > 0;
-
- if (passwordWeak && passwordLeak) {
- const result = await this.dialogService.openSimpleDialog({
- title: { key: "weakAndExposedMasterPassword" },
- content: { key: "weakAndBreachedMasterPasswordDesc" },
- type: "warning",
- });
-
- if (!result) {
- return { isValid: false };
- }
- } else if (passwordWeak) {
- const result = await this.dialogService.openSimpleDialog({
- title: { key: "weakMasterPassword" },
- content: { key: "weakMasterPasswordDesc" },
- type: "warning",
- });
-
- if (!result) {
- return { isValid: false };
- }
- } else if (passwordLeak) {
- const result = await this.dialogService.openSimpleDialog({
- title: { key: "exposedMasterPassword" },
- content: { key: "exposedMasterPasswordDesc" },
- type: "warning",
- });
-
- if (!result) {
- return { isValid: false };
- }
- }
-
- return { isValid: true };
- }
-
- private async buildRegisterRequest(
- email: string,
- masterPassword: string,
- name: string,
- ): Promise {
- const hint = this.formGroup.value.hint;
- const kdfConfig = DEFAULT_KDF_CONFIG;
- const key = await this.keyService.makeMasterKey(masterPassword, email, kdfConfig);
- const newUserKey = await this.keyService.makeUserKey(key);
- const masterKeyHash = await this.keyService.hashMasterKey(masterPassword, key);
- const keys = await this.keyService.makeKeyPair(newUserKey[0]);
- const request = new RegisterRequest(
- email,
- name,
- masterKeyHash,
- hint,
- newUserKey[1].encryptedString,
- this.referenceData,
- this.captchaToken,
- kdfConfig.kdfType,
- kdfConfig.iterations,
- );
- request.keys = new KeysRequest(keys[0], keys[1].encryptedString);
- if (this.modifyRegisterRequest) {
- await this.modifyRegisterRequest(request);
- }
- return request;
- }
-
- private async registerAccount(
- request: RegisterRequest,
- showToast: boolean,
- ): Promise<{ successful: boolean; captchaBypassToken?: string }> {
- if (!(await this.validateRegistration(showToast)).isValid) {
- return { successful: false };
- }
- this.formPromise = this.apiService.postRegister(request);
- try {
- const response = await this.formPromise;
- return { successful: true, captchaBypassToken: response.captchaBypassToken };
- } catch (e) {
- if (this.handleCaptchaRequired(e)) {
- return { successful: false };
- } else {
- throw e;
- }
- }
- }
-
- private async logIn(
- email: string,
- masterPassword: string,
- captchaBypassToken: string,
- ): Promise<{ captchaRequired: boolean }> {
- const credentials = new PasswordLoginCredentials(
- email,
- masterPassword,
- captchaBypassToken,
- null,
- );
- const loginResponse = await this.loginStrategyService.logIn(credentials);
- if (this.handleCaptchaRequired(loginResponse)) {
- return { captchaRequired: true };
- }
- return { captchaRequired: false };
- }
-}
diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts
index d0fc2140f06..5f5e53d8efe 100644
--- a/libs/angular/src/auth/components/sso.component.ts
+++ b/libs/angular/src/auth/components/sso.component.ts
@@ -1,7 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
@@ -28,7 +27,6 @@ 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 { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@@ -57,7 +55,6 @@ export class SsoComponent implements OnInit {
protected redirectUri: string;
protected state: string;
protected codeChallenge: string;
- protected activeUserId: UserId;
constructor(
protected ssoLoginService: SsoLoginServiceAbstraction,
@@ -77,11 +74,7 @@ export class SsoComponent implements OnInit {
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected accountService: AccountService,
protected toastService: ToastService,
- ) {
- this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
- this.activeUserId = account?.id;
- });
- }
+ ) {}
async ngOnInit() {
// eslint-disable-next-line rxjs/no-async-subscribe
@@ -233,10 +226,8 @@ export class SsoComponent implements OnInit {
// - TDE login decryption options component
// - Browser SSO on extension open
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
- await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
- orgSsoIdentifier,
- this.activeUserId,
- );
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
// Users enrolled in admin acct recovery can be forced to set a new password after
// having the admin set a temp password for them (affects TDE & standard users)
diff --git a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts
index 6afee461c42..3e59e4a29b9 100644
--- a/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts
+++ b/libs/angular/src/auth/components/two-factor-auth/two-factor-auth.component.ts
@@ -2,7 +2,6 @@
// @ts-strict-ignore
import { CommonModule } from "@angular/common";
import { Component, Inject, OnInit, ViewChild } from "@angular/core";
-import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { ActivatedRoute, NavigationExtras, Router, RouterLink } from "@angular/router";
import { Subject, takeUntil, lastValueFrom, first, firstValueFrom } from "rxjs";
@@ -32,7 +31,6 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
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 { UserId } from "@bitwarden/common/types/guid";
import {
AsyncActionsModule,
ButtonModule,
@@ -128,7 +126,6 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
protected changePasswordRoute = "set-password";
protected forcePasswordResetRoute = "update-temp-password";
protected successRoute = "vault";
- protected activeUserId: UserId;
constructor(
protected loginStrategyService: LoginStrategyServiceAbstraction,
@@ -151,10 +148,6 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
protected toastService: ToastService,
) {
super(environmentService, i18nService, platformUtilsService, toastService);
-
- this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
- this.activeUserId = account?.id;
- });
}
async ngOnInit() {
@@ -269,10 +262,8 @@ export class TwoFactorAuthComponent extends CaptchaProtectedComponent implements
// Save off the OrgSsoIdentifier for use in the TDE flows
// - TDE login decryption options component
// - Browser SSO on extension open
- await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
- this.orgIdentifier,
- this.activeUserId,
- );
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users
diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts
index 49af9d057f7..e43797332ec 100644
--- a/libs/angular/src/auth/components/two-factor.component.ts
+++ b/libs/angular/src/auth/components/two-factor.component.ts
@@ -35,7 +35,6 @@ 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 { UserId } from "@bitwarden/common/types/guid";
import { ToastService } from "@bitwarden/components";
import { CaptchaProtectedComponent } from "./captcha-protected.component";
@@ -74,8 +73,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected successRoute = "vault";
protected twoFactorTimeoutRoute = "authentication-timeout";
- protected activeUserId: UserId;
-
get isDuoProvider(): boolean {
return (
this.selectedProviderType === TwoFactorProviderType.Duo ||
@@ -108,10 +105,6 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
- this.accountService.activeAccount$.pipe(takeUntilDestroyed()).subscribe((account) => {
- this.activeUserId = account?.id;
- });
-
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
this.loginStrategyService.authenticationSessionTimeout$
.pipe(takeUntilDestroyed())
@@ -295,10 +288,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
// Save off the OrgSsoIdentifier for use in the TDE flows
// - TDE login decryption options component
// - Browser SSO on extension open
- await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
- this.orgIdentifier,
- this.activeUserId,
- );
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(this.orgIdentifier, userId);
this.loginEmailService.clearValues();
// note: this flow affects both TDE & standard users
diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts
index 534a1337eda..e785441b8e4 100644
--- a/libs/angular/src/components/share.component.ts
+++ b/libs/angular/src/components/share.component.ts
@@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@@ -73,10 +74,8 @@ export class ShareComponent implements OnInit, OnDestroy {
}
});
- const cipherDomain = await this.cipherService.get(this.cipherId);
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
this.cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
@@ -104,10 +103,8 @@ export class ShareComponent implements OnInit, OnDestroy {
return;
}
- const cipherDomain = await this.cipherService.get(this.cipherId);
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipherView = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
);
diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts
index 719e3a084f1..d6db49c109d 100644
--- a/libs/angular/src/services/jslib-services.module.ts
+++ b/libs/angular/src/services/jslib-services.module.ts
@@ -296,7 +296,12 @@ import {
DefaultUserAsymmetricKeysRegenerationApiService,
} from "@bitwarden/key-management";
import { SafeInjectionToken } from "@bitwarden/ui-common";
-import { NewDeviceVerificationNoticeService, PasswordRepromptService } from "@bitwarden/vault";
+import {
+ DefaultTaskService,
+ NewDeviceVerificationNoticeService,
+ PasswordRepromptService,
+ TaskService,
+} from "@bitwarden/vault";
import {
VaultExportService,
VaultExportServiceAbstraction,
@@ -1463,6 +1468,11 @@ const safeProviders: SafeProvider[] = [
useClass: PasswordLoginStrategyData,
deps: [],
}),
+ safeProvider({
+ provide: TaskService,
+ useClass: DefaultTaskService,
+ deps: [StateProvider, ApiServiceAbstraction, OrganizationServiceAbstraction, ConfigService],
+ }),
];
@NgModule({
diff --git a/libs/angular/src/tools/generator/components/generator.component.ts b/libs/angular/src/tools/generator/components/generator.component.ts
deleted file mode 100644
index 1f3c635e499..00000000000
--- a/libs/angular/src/tools/generator/components/generator.component.ts
+++ /dev/null
@@ -1,389 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Directive, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from "@angular/core";
-import { ActivatedRoute } from "@angular/router";
-import { BehaviorSubject, combineLatest, firstValueFrom, Subject } from "rxjs";
-import { debounceTime, first, map, skipWhile, takeUntil } from "rxjs/operators";
-
-import { PasswordGeneratorPolicyOptions } from "@bitwarden/common/admin-console/models/domain/password-generator-policy-options";
-import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { ToastService } from "@bitwarden/components";
-import {
- GeneratorType,
- DefaultPasswordBoundaries as DefaultBoundaries,
-} from "@bitwarden/generator-core";
-import {
- PasswordGenerationServiceAbstraction,
- UsernameGenerationServiceAbstraction,
- UsernameGeneratorOptions,
- PasswordGeneratorOptions,
-} from "@bitwarden/generator-legacy";
-
-export class EmailForwarderOptions {
- name: string;
- value: string;
- validForSelfHosted: boolean;
-}
-
-@Directive()
-export class GeneratorComponent implements OnInit, OnDestroy {
- @Input() comingFromAddEdit = false;
- @Input() type: GeneratorType | "";
- @Output() onSelected = new EventEmitter();
-
- usernameGeneratingPromise: Promise;
- typeOptions: any[];
- usernameTypeOptions: any[];
- subaddressOptions: any[];
- catchallOptions: any[];
- forwardOptions: EmailForwarderOptions[];
- usernameOptions: UsernameGeneratorOptions = { website: null };
- passwordOptions: PasswordGeneratorOptions = {};
- username = "-";
- password = "-";
- showOptions = false;
- avoidAmbiguous = false;
- enforcedPasswordPolicyOptions: PasswordGeneratorPolicyOptions;
- usernameWebsite: string = null;
-
- get passTypeOptions() {
- return this._passTypeOptions.filter((o) => !o.disabled);
- }
- private _passTypeOptions: { name: string; value: GeneratorType; disabled: boolean }[];
-
- private destroy$ = new Subject();
- private isInitialized$ = new BehaviorSubject(false);
-
- // update screen reader minimum password length with 500ms debounce
- // so that the user isn't flooded with status updates
- private _passwordOptionsMinLengthForReader = new BehaviorSubject(
- DefaultBoundaries.length.min,
- );
- protected passwordOptionsMinLengthForReader$ = this._passwordOptionsMinLengthForReader.pipe(
- map((val) => val || DefaultBoundaries.length.min),
- debounceTime(500),
- );
-
- private _password = new BehaviorSubject("-");
-
- constructor(
- protected passwordGenerationService: PasswordGenerationServiceAbstraction,
- protected usernameGenerationService: UsernameGenerationServiceAbstraction,
- protected platformUtilsService: PlatformUtilsService,
- protected accountService: AccountService,
- protected i18nService: I18nService,
- protected logService: LogService,
- protected route: ActivatedRoute,
- protected ngZone: NgZone,
- private win: Window,
- protected toastService: ToastService,
- ) {
- this.typeOptions = [
- { name: i18nService.t("password"), value: "password" },
- { name: i18nService.t("username"), value: "username" },
- ];
- this._passTypeOptions = [
- { name: i18nService.t("password"), value: "password", disabled: false },
- { name: i18nService.t("passphrase"), value: "passphrase", disabled: false },
- ];
- this.usernameTypeOptions = [
- {
- name: i18nService.t("plusAddressedEmail"),
- value: "subaddress",
- desc: i18nService.t("plusAddressedEmailDesc"),
- },
- {
- name: i18nService.t("catchallEmail"),
- value: "catchall",
- desc: i18nService.t("catchallEmailDesc"),
- },
- {
- name: i18nService.t("forwardedEmail"),
- value: "forwarded",
- desc: i18nService.t("forwardedEmailDesc"),
- },
- { name: i18nService.t("randomWord"), value: "word" },
- ];
- this.subaddressOptions = [{ name: i18nService.t("random"), value: "random" }];
- this.catchallOptions = [{ name: i18nService.t("random"), value: "random" }];
-
- this.forwardOptions = [
- { name: "", value: "", validForSelfHosted: false },
- { name: "addy.io", value: "anonaddy", validForSelfHosted: true },
- { name: "DuckDuckGo", value: "duckduckgo", validForSelfHosted: false },
- { name: "Fastmail", value: "fastmail", validForSelfHosted: true },
- { name: "Firefox Relay", value: "firefoxrelay", validForSelfHosted: false },
- { name: "SimpleLogin", value: "simplelogin", validForSelfHosted: true },
- { name: "Forward Email", value: "forwardemail", validForSelfHosted: true },
- ].sort((a, b) => a.name.localeCompare(b.name));
-
- this._password.pipe(debounceTime(250)).subscribe((password) => {
- ngZone.run(() => {
- this.password = password;
- });
- this.passwordGenerationService.addHistory(this.password).catch((e) => {
- this.logService.error(e);
- });
- });
- }
-
- cascadeOptions(navigationType: GeneratorType = undefined, accountEmail: string) {
- this.avoidAmbiguous = !this.passwordOptions.ambiguous;
-
- if (!this.type) {
- if (navigationType) {
- this.type = navigationType;
- } else {
- this.type = this.passwordOptions.type === "username" ? "username" : "password";
- }
- }
-
- this.passwordOptions.type =
- this.passwordOptions.type === "passphrase" ? "passphrase" : "password";
-
- const overrideType = this.enforcedPasswordPolicyOptions.overridePasswordType ?? "";
- const isDisabled = overrideType.length
- ? (value: string, policyValue: string) => value !== policyValue
- : (_value: string, _policyValue: string) => false;
- for (const option of this._passTypeOptions) {
- option.disabled = isDisabled(option.value, overrideType);
- }
-
- if (this.usernameOptions.type == null) {
- this.usernameOptions.type = "word";
- }
- if (
- this.usernameOptions.subaddressEmail == null ||
- this.usernameOptions.subaddressEmail === ""
- ) {
- this.usernameOptions.subaddressEmail = accountEmail;
- }
- if (this.usernameWebsite == null) {
- this.usernameOptions.subaddressType = this.usernameOptions.catchallType = "random";
- } else {
- this.usernameOptions.website = this.usernameWebsite;
- }
- }
-
- async ngOnInit() {
- combineLatest([
- this.route.queryParams.pipe(first()),
- this.accountService.activeAccount$.pipe(first()),
- this.passwordGenerationService.getOptions$(),
- this.usernameGenerationService.getOptions$(),
- ])
- .pipe(
- map(([qParams, account, [passwordOptions, passwordPolicy], usernameOptions]) => ({
- navigationType: qParams.type as GeneratorType,
- accountEmail: account.email,
- passwordOptions,
- passwordPolicy,
- usernameOptions,
- })),
- takeUntil(this.destroy$),
- )
- .subscribe((options) => {
- this.passwordOptions = options.passwordOptions;
- this.enforcedPasswordPolicyOptions = options.passwordPolicy;
- this.usernameOptions = options.usernameOptions;
-
- this.cascadeOptions(options.navigationType, options.accountEmail);
- this._passwordOptionsMinLengthForReader.next(this.passwordOptions.minLength);
-
- if (this.regenerateWithoutButtonPress()) {
- this.regenerate().catch((e) => {
- this.logService.error(e);
- });
- }
-
- this.isInitialized$.next(true);
- });
-
- // once initialization is complete, `ngOnInit` should return.
- //
- // FIXME(#6944): if a sync is in progress, wait to complete until after
- // the sync completes.
- await firstValueFrom(
- this.isInitialized$.pipe(
- skipWhile((initialized) => !initialized),
- takeUntil(this.destroy$),
- ),
- );
-
- if (this.usernameWebsite !== null) {
- const websiteOption = { name: this.i18nService.t("websiteName"), value: "website-name" };
- this.subaddressOptions.push(websiteOption);
- this.catchallOptions.push(websiteOption);
- }
- }
-
- ngOnDestroy() {
- this.destroy$.next();
- this.destroy$.complete();
- this.isInitialized$.complete();
- this._passwordOptionsMinLengthForReader.complete();
- }
-
- async typeChanged() {
- await this.savePasswordOptions();
- }
-
- async regenerate() {
- if (this.type === "password") {
- await this.regeneratePassword();
- } else if (this.type === "username") {
- await this.regenerateUsername();
- }
- }
-
- async sliderChanged() {
- // 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.savePasswordOptions();
- await this.passwordGenerationService.addHistory(this.password);
- }
-
- async onPasswordOptionsMinNumberInput($event: Event) {
- // `savePasswordOptions()` replaces the null
- this.passwordOptions.number = null;
-
- await this.savePasswordOptions();
-
- // fixes UI desync that occurs when minNumber has a fixed value
- // that is reset through normalization
- ($event.target as HTMLInputElement).value = `${this.passwordOptions.minNumber}`;
- }
-
- async setPasswordOptionsNumber($event: boolean) {
- this.passwordOptions.number = $event;
- // `savePasswordOptions()` replaces the null
- this.passwordOptions.minNumber = null;
-
- await this.savePasswordOptions();
- }
-
- async onPasswordOptionsMinSpecialInput($event: Event) {
- // `savePasswordOptions()` replaces the null
- this.passwordOptions.special = null;
-
- await this.savePasswordOptions();
-
- // fixes UI desync that occurs when minSpecial has a fixed value
- // that is reset through normalization
- ($event.target as HTMLInputElement).value = `${this.passwordOptions.minSpecial}`;
- }
-
- async setPasswordOptionsSpecial($event: boolean) {
- this.passwordOptions.special = $event;
- // `savePasswordOptions()` replaces the null
- this.passwordOptions.minSpecial = null;
-
- await this.savePasswordOptions();
- }
-
- async sliderInput() {
- await this.normalizePasswordOptions();
- }
-
- async savePasswordOptions() {
- // map navigation state into generator type
- const restoreType = this.passwordOptions.type;
- if (this.type === "username") {
- this.passwordOptions.type = this.type;
- }
-
- // save options
- await this.normalizePasswordOptions();
- await this.passwordGenerationService.saveOptions(this.passwordOptions);
-
- // restore the original format
- this.passwordOptions.type = restoreType;
- }
-
- async saveUsernameOptions() {
- await this.usernameGenerationService.saveOptions(this.usernameOptions);
- if (this.usernameOptions.type === "forwarded") {
- this.username = "-";
- }
- }
-
- async regeneratePassword() {
- this._password.next(
- await this.passwordGenerationService.generatePassword(this.passwordOptions),
- );
- }
-
- regenerateUsername() {
- return this.generateUsername();
- }
-
- async generateUsername() {
- try {
- this.usernameGeneratingPromise = this.usernameGenerationService.generateUsername(
- this.usernameOptions,
- );
- this.username = await this.usernameGeneratingPromise;
- if (this.username === "" || this.username === null) {
- this.username = "-";
- }
- } catch (e) {
- this.logService.error(e);
- }
- }
-
- copy() {
- const password = this.type === "password";
- const copyOptions = this.win != null ? { window: this.win } : null;
- this.platformUtilsService.copyToClipboard(
- password ? this.password : this.username,
- copyOptions,
- );
- this.toastService.showToast({
- variant: "info",
- title: null,
- message: this.i18nService.t(
- "valueCopied",
- this.i18nService.t(password ? "password" : "username"),
- ),
- });
- }
-
- select() {
- this.onSelected.emit(this.type === "password" ? this.password : this.username);
- }
-
- toggleOptions() {
- this.showOptions = !this.showOptions;
- }
-
- regenerateWithoutButtonPress() {
- return this.type !== "username" || this.usernameOptions.type !== "forwarded";
- }
-
- private async normalizePasswordOptions() {
- // Application level normalize options dependent on class variables
- this.passwordOptions.ambiguous = !this.avoidAmbiguous;
-
- if (
- !this.passwordOptions.uppercase &&
- !this.passwordOptions.lowercase &&
- !this.passwordOptions.number &&
- !this.passwordOptions.special
- ) {
- this.passwordOptions.lowercase = true;
- if (this.win != null) {
- const lowercase = this.win.document.querySelector("#lowercase") as HTMLInputElement;
- if (lowercase) {
- lowercase.checked = true;
- }
- }
- }
-
- await this.passwordGenerationService.enforcePasswordGeneratorPoliciesOnOptions(
- this.passwordOptions,
- );
- }
-}
diff --git a/libs/angular/src/tools/generator/components/password-generator-history.component.ts b/libs/angular/src/tools/generator/components/password-generator-history.component.ts
deleted file mode 100644
index 2933163fce2..00000000000
--- a/libs/angular/src/tools/generator/components/password-generator-history.component.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
-import { Directive, OnInit } from "@angular/core";
-
-import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
-import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
-import { ToastService } from "@bitwarden/components";
-import { GeneratedPasswordHistory } from "@bitwarden/generator-history";
-import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
-
-@Directive()
-export class PasswordGeneratorHistoryComponent implements OnInit {
- history: GeneratedPasswordHistory[] = [];
-
- constructor(
- protected passwordGenerationService: PasswordGenerationServiceAbstraction,
- protected platformUtilsService: PlatformUtilsService,
- protected i18nService: I18nService,
- private win: Window,
- protected toastService: ToastService,
- ) {}
-
- async ngOnInit() {
- this.history = await this.passwordGenerationService.getHistory();
- }
-
- clear = async () => {
- this.history = await this.passwordGenerationService.clear();
- };
-
- copy(password: string) {
- const copyOptions = this.win != null ? { window: this.win } : null;
- this.platformUtilsService.copyToClipboard(password, copyOptions);
- this.toastService.showToast({
- variant: "info",
- title: null,
- message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
- });
- }
-}
diff --git a/libs/angular/src/tools/generator/generator-swap.ts b/libs/angular/src/tools/generator/generator-swap.ts
deleted file mode 100644
index 16fafc67116..00000000000
--- a/libs/angular/src/tools/generator/generator-swap.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Type, inject } from "@angular/core";
-import { Route, Routes } from "@angular/router";
-
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-
-import { componentRouteSwap } from "../../utils/component-route-swap";
-
-/**
- * Helper function to swap between two components based on the GeneratorToolsModernization feature flag.
- * @param defaultComponent - The current non-refreshed component to render.
- * @param refreshedComponent - The new refreshed component to render.
- * @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
- * @param altOptions - The alt route options to apply to the alt component.
- */
-export function generatorSwap(
- defaultComponent: Type,
- refreshedComponent: Type,
- options: Route,
- altOptions?: Route,
-): Routes {
- return componentRouteSwap(
- defaultComponent,
- refreshedComponent,
- async () => {
- const configService = inject(ConfigService);
- return configService.getFeatureFlag(FeatureFlag.GeneratorToolsModernization);
- },
- options,
- altOptions,
- );
-}
diff --git a/libs/angular/src/utils/extension-refresh-redirect.spec.ts b/libs/angular/src/utils/extension-refresh-redirect.spec.ts
deleted file mode 100644
index 3291a4496ff..00000000000
--- a/libs/angular/src/utils/extension-refresh-redirect.spec.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { TestBed } from "@angular/core/testing";
-import { Navigation, Router, UrlTree } from "@angular/router";
-import { mock, MockProxy } from "jest-mock-extended";
-
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-
-import { extensionRefreshRedirect } from "./extension-refresh-redirect";
-
-describe("extensionRefreshRedirect", () => {
- let configService: MockProxy;
- let router: MockProxy;
-
- beforeEach(() => {
- configService = mock();
- router = mock();
-
- TestBed.configureTestingModule({
- providers: [
- { provide: ConfigService, useValue: configService },
- { provide: Router, useValue: router },
- ],
- });
- });
-
- it("returns true when ExtensionRefresh flag is disabled", async () => {
- configService.getFeatureFlag.mockResolvedValue(false);
-
- const result = await TestBed.runInInjectionContext(() =>
- extensionRefreshRedirect("/redirect")(),
- );
-
- expect(result).toBe(true);
- expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
- expect(router.parseUrl).not.toHaveBeenCalled();
- });
-
- it("returns UrlTree when ExtensionRefresh flag is enabled and preserves query params", async () => {
- configService.getFeatureFlag.mockResolvedValue(true);
-
- const urlTree = new UrlTree();
- urlTree.queryParams = { test: "test" };
-
- const navigation: Navigation = {
- extras: {},
- id: 0,
- initialUrl: new UrlTree(),
- extractedUrl: urlTree,
- trigger: "imperative",
- previousNavigation: undefined,
- };
-
- router.getCurrentNavigation.mockReturnValue(navigation);
-
- await TestBed.runInInjectionContext(() => extensionRefreshRedirect("/redirect")());
-
- expect(configService.getFeatureFlag).toHaveBeenCalledWith(FeatureFlag.ExtensionRefresh);
- expect(router.createUrlTree).toHaveBeenCalledWith(["/redirect"], {
- queryParams: urlTree.queryParams,
- });
- });
-});
diff --git a/libs/angular/src/utils/extension-refresh-redirect.ts b/libs/angular/src/utils/extension-refresh-redirect.ts
deleted file mode 100644
index 2baa3a3ec89..00000000000
--- a/libs/angular/src/utils/extension-refresh-redirect.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { inject } from "@angular/core";
-import { UrlTree, Router } from "@angular/router";
-
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-
-/**
- * Helper function to redirect to a new URL based on the ExtensionRefresh feature flag.
- * @param redirectUrl - The URL to redirect to if the ExtensionRefresh flag is enabled.
- */
-export function extensionRefreshRedirect(redirectUrl: string): () => Promise {
- return async () => {
- const configService = inject(ConfigService);
- const router = inject(Router);
-
- const shouldRedirect = await configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
- if (shouldRedirect) {
- const currentNavigation = router.getCurrentNavigation();
- const queryParams = currentNavigation?.extractedUrl?.queryParams || {};
-
- // Preserve query params when redirecting as it is likely that the refreshed component
- // will be consuming the same query params.
- return router.createUrlTree([redirectUrl], { queryParams });
- } else {
- return true;
- }
- };
-}
diff --git a/libs/angular/src/utils/extension-refresh-swap.ts b/libs/angular/src/utils/extension-refresh-swap.ts
deleted file mode 100644
index 6512be032d2..00000000000
--- a/libs/angular/src/utils/extension-refresh-swap.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Type, inject } from "@angular/core";
-import { Route, Routes } from "@angular/router";
-
-import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
-import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
-
-import { componentRouteSwap } from "./component-route-swap";
-
-/**
- * Helper function to swap between two components based on the ExtensionRefresh feature flag.
- * @param defaultComponent - The current non-refreshed component to render.
- * @param refreshedComponent - The new refreshed component to render.
- * @param options - The shared route options to apply to the default component, and to the alt component if altOptions is not provided.
- * @param altOptions - The alt route options to apply to the alt component.
- */
-export function extensionRefreshSwap(
- defaultComponent: Type,
- refreshedComponent: Type,
- options: Route,
- altOptions?: Route,
-): Routes {
- return componentRouteSwap(
- defaultComponent,
- refreshedComponent,
- async () => {
- const configService = inject(ConfigService);
- return configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
- },
- options,
- altOptions,
- );
-}
diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts
index 26f645d89ef..923f667e680 100644
--- a/libs/angular/src/vault/components/add-edit.component.ts
+++ b/libs/angular/src/vault/components/add-edit.component.ts
@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -101,8 +102,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
private personalOwnershipPolicyAppliesToActiveUser: boolean;
private previousCipherId: string;
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform(
@@ -125,7 +124,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected policyService: PolicyService,
protected logService: LogService,
protected passwordRepromptService: PasswordRepromptService,
- protected organizationService: OrganizationService,
+ private organizationService: OrganizationService,
protected dialogService: DialogService,
protected win: Window,
protected datePipe: DatePipe,
@@ -263,12 +262,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.title = this.i18nService.t("addItem");
}
- const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+
+ const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(activeUserId);
- const activeUserId = await firstValueFrom(this.activeUserId$);
if (this.cipher == null) {
if (this.editMode) {
- const cipher = await this.loadCipher();
+ const cipher = await this.loadCipher(activeUserId);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
@@ -420,9 +420,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.cipher.id = null;
}
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const cipher = await this.encryptCipher(activeUserId);
try {
this.formPromise = this.saveCipher(cipher);
@@ -516,7 +514,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
try {
- this.deletePromise = this.deleteCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ this.deletePromise = this.deleteCipher(activeUserId);
await this.deletePromise;
this.toastService.showToast({
variant: "success",
@@ -542,7 +541,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
try {
- this.restorePromise = this.restoreCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ this.restorePromise = this.restoreCipher(activeUserId);
await this.restorePromise;
this.toastService.showToast({
variant: "success",
@@ -725,8 +725,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
return allCollections.filter((c) => !c.readOnly);
}
- protected loadCipher() {
- return this.cipherService.get(this.cipherId);
+ protected loadCipher(userId: UserId) {
+ return this.cipherService.get(this.cipherId, userId);
}
protected encryptCipher(userId: UserId) {
@@ -746,14 +746,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
: this.cipherService.updateWithServer(cipher, orgAdmin);
}
- protected deleteCipher() {
+ protected deleteCipher(userId: UserId) {
return this.cipher.isDeleted
- ? this.cipherService.deleteWithServer(this.cipher.id, this.asAdmin)
- : this.cipherService.softDeleteWithServer(this.cipher.id, this.asAdmin);
+ ? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin)
+ : this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin);
}
- protected restoreCipher() {
- return this.cipherService.restoreWithServer(this.cipher.id, this.asAdmin);
+ protected restoreCipher(userId: UserId) {
+ return this.cipherService.restoreWithServer(this.cipher.id, userId, this.asAdmin);
}
/**
@@ -773,8 +773,10 @@ export class AddEditComponent implements OnInit, OnDestroy {
return this.ownershipOptions[0].value;
}
- async loadAddEditCipherInfo(): Promise {
- const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$);
+ async loadAddEditCipherInfo(userId: UserId): Promise {
+ const addEditCipherInfo: any = await firstValueFrom(
+ this.cipherService.addEditCipherInfo$(userId),
+ );
const loadedSavedInfo = addEditCipherInfo != null;
if (loadedSavedInfo) {
@@ -787,7 +789,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
}
}
- await this.cipherService.setAddEditCipherInfo(null);
+ await this.cipherService.setAddEditCipherInfo(null, userId);
return loadedSavedInfo;
}
diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts
index 9f1dd31da0c..b1bfcde852a 100644
--- a/libs/angular/src/vault/components/attachments.component.ts
+++ b/libs/angular/src/vault/components/attachments.component.ts
@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core";
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -84,9 +85,7 @@ export class AttachmentsComponent implements OnInit {
}
try {
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
this.cipherDomain = await this.formPromise;
this.cipher = await this.cipherDomain.decrypt(
@@ -125,12 +124,11 @@ export class AttachmentsComponent implements OnInit {
}
try {
- this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
+ const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
+
+ this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id, activeUserId);
const updatedCipher = await this.deletePromises[attachment.id];
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
const cipher = new Cipher(updatedCipher);
this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
@@ -228,10 +226,8 @@ export class AttachmentsComponent implements OnInit {
}
protected async init() {
- this.cipherDomain = await this.loadCipher();
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ this.cipherDomain = await this.loadCipher(activeUserId);
this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
);
@@ -287,7 +283,7 @@ export class AttachmentsComponent implements OnInit {
: await this.keyService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ this.accountService.activeAccount$.pipe(getUserId),
);
this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer(
this.cipherDomain,
@@ -301,7 +297,10 @@ export class AttachmentsComponent implements OnInit {
);
// 3. Delete old
- this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id);
+ this.deletePromises[attachment.id] = this.deleteCipherAttachment(
+ attachment.id,
+ activeUserId,
+ );
await this.deletePromises[attachment.id];
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
if (foundAttachment.length > 0) {
@@ -335,16 +334,16 @@ export class AttachmentsComponent implements OnInit {
}
}
- protected loadCipher() {
- return this.cipherService.get(this.cipherId);
+ protected loadCipher(userId: UserId) {
+ return this.cipherService.get(this.cipherId, userId);
}
protected saveCipherAttachment(file: File, userId: UserId) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
}
- protected deleteCipherAttachment(attachmentId: string) {
- return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId);
+ protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
+ return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId, userId);
}
protected async reupload(attachment: AttachmentView) {
diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts
index 0b385688d0b..4df9f4bd24d 100644
--- a/libs/angular/src/vault/components/password-history.component.ts
+++ b/libs/angular/src/vault/components/password-history.component.ts
@@ -1,9 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, OnInit } from "@angular/core";
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -39,10 +40,8 @@ export class PasswordHistoryComponent implements OnInit {
}
protected async init() {
- const cipher = await this.cipherService.get(this.cipherId);
- const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
- );
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const cipher = await this.cipherService.get(this.cipherId, activeUserId);
const decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts
index f093aeb1330..fb76ff500eb 100644
--- a/libs/angular/src/vault/components/vault-items.component.ts
+++ b/libs/angular/src/vault/components/vault-items.component.ts
@@ -2,10 +2,13 @@
// @ts-strict-ignore
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
-import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs";
+import { BehaviorSubject, Subject, firstValueFrom, from, map, switchMap, takeUntil } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -41,11 +44,20 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
constructor(
protected searchService: SearchService,
protected cipherService: CipherService,
+ protected accountService: AccountService,
) {
- this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => {
- void this.doSearch(ciphers);
- this.loaded = true;
- });
+ this.accountService.activeAccount$
+ .pipe(
+ getUserId,
+ switchMap((userId) =>
+ this.cipherService.cipherViews$(userId).pipe(map((ciphers) => ({ userId, ciphers }))),
+ ),
+ takeUntilDestroyed(),
+ )
+ .subscribe(({ userId, ciphers }) => {
+ void this.doSearch(ciphers, userId);
+ this.loaded = true;
+ });
}
ngOnInit(): void {
@@ -122,10 +134,16 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
- protected async doSearch(indexedCiphers?: CipherView[]) {
- indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$));
+ protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) {
+ // Get userId from activeAccount if not provided from parent stream
+ if (!userId) {
+ userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
+ }
- const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$);
+ indexedCiphers =
+ indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId)));
+
+ const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId));
if (failedCiphers != null && failedCiphers.length > 0) {
indexedCiphers = [...failedCiphers, ...indexedCiphers];
}
diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts
index 227bc14f1b1..637596256b0 100644
--- a/libs/angular/src/vault/components/view.component.ts
+++ b/libs/angular/src/vault/components/view.component.ts
@@ -11,13 +11,14 @@ import {
OnInit,
Output,
} from "@angular/core";
-import { filter, firstValueFrom, map, Observable } from "rxjs";
+import { filter, firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
+import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
@@ -29,11 +30,11 @@ 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 { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
-import { CollectionId } from "@bitwarden/common/types/guid";
+import { CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
-import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
+import { CipherType, FieldType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Launchable } from "@bitwarden/common/vault/interfaces/launchable";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
@@ -65,7 +66,6 @@ export class ViewComponent implements OnDestroy, OnInit {
showPrivateKey: boolean;
canAccessPremium: boolean;
showPremiumRequiredTotp: boolean;
- showUpgradeRequiredTotp: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
@@ -80,7 +80,7 @@ export class ViewComponent implements OnDestroy, OnInit {
private previousCipherId: string;
private passwordReprompted = false;
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
+ private destroyed$ = new Subject();
get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated");
@@ -144,38 +144,39 @@ export class ViewComponent implements OnDestroy, OnInit {
async load() {
this.cleanUp();
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// Grab individual cipher from `cipherViews$` for the most up-to-date information
- this.cipher = await firstValueFrom(
- this.cipherService.cipherViews$.pipe(
- map((ciphers) => ciphers.find((c) => c.id === this.cipherId)),
+ this.cipherService
+ .cipherViews$(activeUserId)
+ .pipe(
+ map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
filter((cipher) => !!cipher),
- ),
- );
+ takeUntil(this.destroyed$),
+ )
+ .subscribe((cipher) => {
+ this.cipher = cipher;
+ });
this.canAccessPremium = await firstValueFrom(
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
this.showPremiumRequiredTotp =
- this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId;
+ this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.collectionId as CollectionId,
]);
- this.showUpgradeRequiredTotp =
- this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp;
-
if (this.cipher.folderId) {
this.folder = await (
await firstValueFrom(this.folderService.folderViews$(activeUserId))
).find((f) => f.id == this.cipher.folderId);
}
- const canGenerateTotp = this.cipher.organizationId
- ? this.cipher.organizationUseTotp
- : this.canAccessPremium;
-
- if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) {
+ if (
+ this.cipher.type === CipherType.Login &&
+ this.cipher.login.totp &&
+ (this.cipher.organizationUseTotp || this.canAccessPremium)
+ ) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
@@ -250,7 +251,8 @@ export class ViewComponent implements OnDestroy, OnInit {
}
try {
- await this.deleteCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.deleteCipher(activeUserId);
this.toastService.showToast({
variant: "success",
title: null,
@@ -272,7 +274,8 @@ export class ViewComponent implements OnDestroy, OnInit {
}
try {
- await this.restoreCipher();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.restoreCipher(activeUserId);
this.toastService.showToast({
variant: "success",
title: null,
@@ -380,7 +383,8 @@ export class ViewComponent implements OnDestroy, OnInit {
}
if (cipherId) {
- await this.cipherService.updateLastLaunchedDate(cipherId);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ await this.cipherService.updateLastLaunchedDate(cipherId, activeUserId);
}
this.platformUtilsService.launchUri(uri.launchUri);
@@ -498,14 +502,14 @@ export class ViewComponent implements OnDestroy, OnInit {
a.downloading = false;
}
- protected deleteCipher() {
+ protected deleteCipher(userId: UserId) {
return this.cipher.isDeleted
- ? this.cipherService.deleteWithServer(this.cipher.id)
- : this.cipherService.softDeleteWithServer(this.cipher.id);
+ ? this.cipherService.deleteWithServer(this.cipher.id, userId)
+ : this.cipherService.softDeleteWithServer(this.cipher.id, userId);
}
- protected restoreCipher() {
- return this.cipherService.restoreWithServer(this.cipher.id);
+ protected restoreCipher(userId: UserId) {
+ return this.cipherService.restoreWithServer(this.cipher.id, userId);
}
protected async promptPassword() {
@@ -524,6 +528,7 @@ export class ViewComponent implements OnDestroy, OnInit {
this.showCardNumber = false;
this.showCardCode = false;
this.passwordReprompted = false;
+ this.destroyed$.next();
if (this.totpInterval) {
clearInterval(this.totpInterval);
}
diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts
index 960590dab53..938d5b32527 100644
--- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts
+++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.spec.ts
@@ -2,9 +2,8 @@ import { TestBed } from "@angular/core/testing";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { BehaviorSubject } from "rxjs";
-import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
-import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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";
@@ -36,17 +35,21 @@ describe("NewDeviceVerificationNoticeGuard", () => {
});
const isSelfHost = jest.fn().mockReturnValue(false);
const getProfileTwoFactorEnabled = jest.fn().mockResolvedValue(false);
- const policyAppliesToActiveUser$ = jest.fn().mockReturnValue(new BehaviorSubject(false));
const noticeState$ = jest.fn().mockReturnValue(new BehaviorSubject(null));
const getProfileCreationDate = jest.fn().mockResolvedValue(eightDaysAgo);
+ const hasMasterPasswordAndMasterKeyHash = jest.fn().mockResolvedValue(true);
+ const getUserSSOBound = jest.fn().mockResolvedValue(false);
+ const getUserSSOBoundAdminOwner = jest.fn().mockResolvedValue(false);
beforeEach(() => {
getFeatureFlag.mockClear();
isSelfHost.mockClear();
getProfileCreationDate.mockClear();
getProfileTwoFactorEnabled.mockClear();
- policyAppliesToActiveUser$.mockClear();
createUrlTree.mockClear();
+ hasMasterPasswordAndMasterKeyHash.mockClear();
+ getUserSSOBound.mockClear();
+ getUserSSOBoundAdminOwner.mockClear();
TestBed.configureTestingModule({
providers: [
@@ -55,10 +58,15 @@ describe("NewDeviceVerificationNoticeGuard", () => {
{ provide: NewDeviceVerificationNoticeService, useValue: { noticeState$ } },
{ provide: AccountService, useValue: { activeAccount$ } },
{ provide: PlatformUtilsService, useValue: { isSelfHost } },
- { provide: PolicyService, useValue: { policyAppliesToActiveUser$ } },
+ { provide: UserVerificationService, useValue: { hasMasterPasswordAndMasterKeyHash } },
{
provide: VaultProfileService,
- useValue: { getProfileCreationDate, getProfileTwoFactorEnabled },
+ useValue: {
+ getProfileCreationDate,
+ getProfileTwoFactorEnabled,
+ getUserSSOBound,
+ getUserSSOBoundAdminOwner,
+ },
},
],
});
@@ -90,7 +98,7 @@ describe("NewDeviceVerificationNoticeGuard", () => {
expect(isSelfHost).not.toHaveBeenCalled();
expect(getProfileTwoFactorEnabled).not.toHaveBeenCalled();
expect(getProfileCreationDate).not.toHaveBeenCalled();
- expect(policyAppliesToActiveUser$).not.toHaveBeenCalled();
+ expect(hasMasterPasswordAndMasterKeyHash).not.toHaveBeenCalled();
});
});
@@ -121,13 +129,6 @@ describe("NewDeviceVerificationNoticeGuard", () => {
expect(await newDeviceGuard()).toBe(true);
});
- it("returns `true` SSO is required", async () => {
- policyAppliesToActiveUser$.mockReturnValueOnce(new BehaviorSubject(true));
-
- expect(await newDeviceGuard()).toBe(true);
- expect(policyAppliesToActiveUser$).toHaveBeenCalledWith(PolicyType.RequireSso);
- });
-
it("returns `true` when the profile was created less than a week ago", async () => {
const sixDaysAgo = new Date();
sixDaysAgo.setDate(sixDaysAgo.getDate() - 6);
@@ -143,6 +144,57 @@ describe("NewDeviceVerificationNoticeGuard", () => {
expect(await newDeviceGuard()).toBe(true);
});
+ describe("SSO bound", () => {
+ beforeEach(() => {
+ getFeatureFlag.mockImplementation((key) => {
+ if (key === FeatureFlag.NewDeviceVerificationPermanentDismiss) {
+ return Promise.resolve(true);
+ }
+
+ return Promise.resolve(false);
+ });
+ });
+
+ afterAll(() => {
+ getFeatureFlag.mockReturnValue(false);
+ });
+
+ it('returns "true" when the user is SSO bound and not an admin or owner', async () => {
+ getUserSSOBound.mockResolvedValueOnce(true);
+ getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
+
+ expect(await newDeviceGuard()).toBe(true);
+ });
+
+ it('returns "true" when the user is an admin or owner of an SSO bound organization and has not logged in with their master password', async () => {
+ getUserSSOBound.mockResolvedValueOnce(true);
+ getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
+ hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(false);
+
+ expect(await newDeviceGuard()).toBe(true);
+ });
+
+ it("shows notice when the user is an admin or owner of an SSO bound organization and logged in with their master password", async () => {
+ getUserSSOBound.mockResolvedValueOnce(true);
+ getUserSSOBoundAdminOwner.mockResolvedValueOnce(true);
+ hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
+
+ await newDeviceGuard();
+
+ expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
+ });
+
+ it("shows notice when the user that is not in an SSO bound organization", async () => {
+ getUserSSOBound.mockResolvedValueOnce(false);
+ getUserSSOBoundAdminOwner.mockResolvedValueOnce(false);
+ hasMasterPasswordAndMasterKeyHash.mockResolvedValueOnce(true);
+
+ await newDeviceGuard();
+
+ expect(createUrlTree).toHaveBeenCalledWith(["/new-device-notice"]);
+ });
+ });
+
describe("temp flag", () => {
beforeEach(() => {
getFeatureFlag.mockImplementation((key) => {
diff --git a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts
index 09d6b3313c4..8d4a7742bc5 100644
--- a/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts
+++ b/libs/angular/src/vault/guards/new-device-verification-notice.guard.ts
@@ -2,9 +2,8 @@ import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivateFn, Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs";
-import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
-import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
+import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
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";
@@ -20,8 +19,8 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
const newDeviceVerificationNoticeService = inject(NewDeviceVerificationNoticeService);
const accountService = inject(AccountService);
const platformUtilsService = inject(PlatformUtilsService);
- const policyService = inject(PolicyService);
const vaultProfileService = inject(VaultProfileService);
+ const userVerificationService = inject(UserVerificationService);
if (route.queryParams["fromNewDeviceVerification"]) {
return true;
@@ -47,7 +46,11 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
try {
const isSelfHosted = platformUtilsService.isSelfHost();
- const requiresSSO = await isSSORequired(policyService);
+ const userIsSSOUser = await ssoAppliesToUser(
+ userVerificationService,
+ vaultProfileService,
+ currentAcct.id,
+ );
const has2FAEnabled = await hasATwoFactorProviderEnabled(vaultProfileService, currentAcct.id);
const isProfileLessThanWeekOld = await profileIsLessThanWeekOld(
vaultProfileService,
@@ -55,8 +58,9 @@ export const NewDeviceVerificationNoticeGuard: CanActivateFn = async (
);
// When any of the following are true, the device verification notice is
- // not applicable for the user.
- if (has2FAEnabled || isSelfHosted || requiresSSO || isProfileLessThanWeekOld) {
+ // not applicable for the user. When the user has *not* logged in with their
+ // master password, assume they logged in with SSO.
+ if (has2FAEnabled || isSelfHosted || userIsSSOUser || isProfileLessThanWeekOld) {
return true;
}
} catch {
@@ -105,9 +109,39 @@ async function profileIsLessThanWeekOld(
return !isMoreThan7DaysAgo(creationDate);
}
-/** Returns true when the user is required to login via SSO */
-async function isSSORequired(policyService: PolicyService) {
- return firstValueFrom(policyService.policyAppliesToActiveUser$(PolicyType.RequireSso));
+/**
+ * Returns true when either:
+ * - The user is SSO bound to an organization and is not an Admin or Owner
+ * - The user is an Admin or Owner of an organization with SSO bound and has not logged in with their master password
+ *
+ * NOTE: There are edge cases where this does not satisfy the original requirement of showing the notice to
+ * users who are subject to the SSO required policy. When Owners and Admins log in with their MP they will see the notice
+ * when they log in with SSO they will not. This is a concession made because the original logic references policies would not work for TDE users.
+ * When this guard is run for those users a sync hasn't occurred and thus the policies are not available.
+ */
+async function ssoAppliesToUser(
+ userVerificationService: UserVerificationService,
+ vaultProfileService: VaultProfileService,
+ userId: string,
+) {
+ const userSSOBound = await vaultProfileService.getUserSSOBound(userId);
+ const userSSOBoundAdminOwner = await vaultProfileService.getUserSSOBoundAdminOwner(userId);
+ const userLoggedInWithMP = await userLoggedInWithMasterPassword(userVerificationService, userId);
+
+ const nonOwnerAdminSsoUser = userSSOBound && !userSSOBoundAdminOwner;
+ const ssoAdminOwnerLoggedInWithMP = userSSOBoundAdminOwner && !userLoggedInWithMP;
+
+ return nonOwnerAdminSsoUser || ssoAdminOwnerLoggedInWithMP;
+}
+
+/**
+ * Returns true when the user logged in with their master password.
+ */
+async function userLoggedInWithMasterPassword(
+ userVerificationService: UserVerificationService,
+ userId: string,
+) {
+ return userVerificationService.hasMasterPasswordAndMasterKeyHash(userId);
}
/** Returns the true when the date given is older than 7 days */
diff --git a/libs/angular/src/vault/services/vault-profile.service.spec.ts b/libs/angular/src/vault/services/vault-profile.service.spec.ts
index 7761503253a..ade34da39a6 100644
--- a/libs/angular/src/vault/services/vault-profile.service.spec.ts
+++ b/libs/angular/src/vault/services/vault-profile.service.spec.ts
@@ -1,6 +1,7 @@
import { TestBed } from "@angular/core/testing";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { VaultProfileService } from "./vault-profile.service";
@@ -13,6 +14,12 @@ describe("VaultProfileService", () => {
creationDate: hardcodedDateString,
twoFactorEnabled: true,
id: "new-user-id",
+ organizations: [
+ {
+ ssoBound: true,
+ type: OrganizationUserType.Admin,
+ },
+ ],
});
beforeEach(() => {
@@ -91,4 +98,64 @@ describe("VaultProfileService", () => {
expect(getProfile).not.toHaveBeenCalled();
});
});
+
+ describe("getUserSSOBound", () => {
+ it("calls `getProfile` when stored ssoBound property is not stored", async () => {
+ expect(service["userIsSsoBound"]).toBeNull();
+
+ const userIsSsoBound = await service.getUserSSOBound(userId);
+
+ expect(userIsSsoBound).toBe(true);
+ expect(getProfile).toHaveBeenCalled();
+ });
+
+ it("calls `getProfile` when stored profile id does not match", async () => {
+ service["userIsSsoBound"] = false;
+ service["userId"] = "old-user-id";
+
+ const userIsSsoBound = await service.getUserSSOBound(userId);
+
+ expect(userIsSsoBound).toBe(true);
+ expect(getProfile).toHaveBeenCalled();
+ });
+
+ it("does not call `getProfile` when ssoBound property is already stored", async () => {
+ service["userIsSsoBound"] = false;
+
+ const userIsSsoBound = await service.getUserSSOBound(userId);
+
+ expect(userIsSsoBound).toBe(false);
+ expect(getProfile).not.toHaveBeenCalled();
+ });
+ });
+
+ describe("getUserSSOBoundAdminOwner", () => {
+ it("calls `getProfile` when stored userIsSsoBoundAdminOwner property is not stored", async () => {
+ expect(service["userIsSsoBoundAdminOwner"]).toBeNull();
+
+ const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
+
+ expect(userIsSsoBoundAdminOwner).toBe(true);
+ expect(getProfile).toHaveBeenCalled();
+ });
+
+ it("calls `getProfile` when stored profile id does not match", async () => {
+ service["userIsSsoBoundAdminOwner"] = false;
+ service["userId"] = "old-user-id";
+
+ const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
+
+ expect(userIsSsoBoundAdminOwner).toBe(true);
+ expect(getProfile).toHaveBeenCalled();
+ });
+
+ it("does not call `getProfile` when userIsSsoBoundAdminOwner property is already stored", async () => {
+ service["userIsSsoBoundAdminOwner"] = false;
+
+ const userIsSsoBoundAdminOwner = await service.getUserSSOBoundAdminOwner(userId);
+
+ expect(userIsSsoBoundAdminOwner).toBe(false);
+ expect(getProfile).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/libs/angular/src/vault/services/vault-profile.service.ts b/libs/angular/src/vault/services/vault-profile.service.ts
index b368a973781..21f4ecc2285 100644
--- a/libs/angular/src/vault/services/vault-profile.service.ts
+++ b/libs/angular/src/vault/services/vault-profile.service.ts
@@ -1,6 +1,7 @@
import { Injectable, inject } from "@angular/core";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
+import { OrganizationUserType } from "@bitwarden/common/admin-console/enums";
import { ProfileResponse } from "@bitwarden/common/models/response/profile.response";
@Injectable({
@@ -24,6 +25,12 @@ export class VaultProfileService {
/** True when 2FA is enabled on the profile. */
private profile2FAEnabled: boolean | null = null;
+ /** True when ssoBound is true for any of the users organizations */
+ private userIsSsoBound: boolean | null = null;
+
+ /** True when the user is an admin or owner of the ssoBound organization */
+ private userIsSsoBoundAdminOwner: boolean | null = null;
+
/**
* Returns the creation date of the profile.
* Note: `Date`s are mutable in JS, creating a new
@@ -52,12 +59,43 @@ export class VaultProfileService {
return profile.twoFactorEnabled;
}
+ /**
+ * Returns whether the user logs in with SSO for any organization.
+ */
+ async getUserSSOBound(userId: string): Promise {
+ if (this.userIsSsoBound !== null && userId === this.userId) {
+ return Promise.resolve(this.userIsSsoBound);
+ }
+
+ await this.fetchAndCacheProfile();
+
+ return !!this.userIsSsoBound;
+ }
+
+ /**
+ * Returns true when the user is an Admin or Owner of an organization with `ssoBound` true.
+ */
+ async getUserSSOBoundAdminOwner(userId: string): Promise {
+ if (this.userIsSsoBoundAdminOwner !== null && userId === this.userId) {
+ return Promise.resolve(this.userIsSsoBoundAdminOwner);
+ }
+
+ await this.fetchAndCacheProfile();
+
+ return !!this.userIsSsoBoundAdminOwner;
+ }
+
private async fetchAndCacheProfile(): Promise {
const profile = await this.apiService.getProfile();
this.userId = profile.id;
this.profileCreatedDate = profile.creationDate;
this.profile2FAEnabled = profile.twoFactorEnabled;
+ const ssoBoundOrg = profile.organizations.find((org) => org.ssoBound);
+ this.userIsSsoBound = !!ssoBoundOrg;
+ this.userIsSsoBoundAdminOwner =
+ ssoBoundOrg?.type === OrganizationUserType.Admin ||
+ ssoBoundOrg?.type === OrganizationUserType.Owner;
return profile;
}
diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts
index aab06a69add..d175942c475 100644
--- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts
+++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts
@@ -11,6 +11,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state";
+import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -30,8 +31,6 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
private readonly collapsedGroupings$: Observable> =
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
- private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
-
constructor(
protected organizationService: OrganizationService,
protected folderService: FolderService,
@@ -63,7 +62,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
}
buildNestedFolders(organizationId?: string): Observable> {
- const transformation = async (storedFolders: FolderView[]) => {
+ const transformation = async (storedFolders: FolderView[], userId: UserId) => {
let folders: FolderView[];
// If no org or "My Vault" is selected, show all folders
@@ -71,7 +70,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
folders = storedFolders;
} else {
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder
- const ciphers = await this.cipherService.getAllDecrypted();
+ const ciphers = await this.cipherService.getAllDecrypted(userId);
const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
folders = storedFolders.filter(
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
@@ -85,9 +84,13 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
});
};
- return this.activeUserId$.pipe(
- switchMap((userId) => this.folderService.folderViews$(userId)),
- mergeMap((folders) => from(transformation(folders))),
+ return this.accountService.activeAccount$.pipe(
+ getUserId,
+ switchMap((userId) =>
+ this.folderService
+ .folderViews$(userId)
+ .pipe(mergeMap((folders) => from(transformation(folders, userId)))),
+ ),
);
}
@@ -131,7 +134,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
}
async getFolderNested(id: string): Promise> {
- const activeUserId = await firstValueFrom(this.activeUserId$);
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const folders = await this.getAllFoldersNested(
await firstValueFrom(this.folderService.folderViews$(activeUserId)),
);
diff --git a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html
index b3d218389bf..bf37380dc39 100644
--- a/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html
+++ b/libs/auth/src/angular/login-decryption-options/login-decryption-options.component.html
@@ -1,5 +1,5 @@
-
+
| undefined;
initiateSsoFormPromise: Promise | undefined;
@@ -132,8 +130,6 @@ export class SsoComponent implements OnInit {
}
async ngOnInit() {
- this.activeUserId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
-
const qParams: QueryParams = await firstValueFrom(this.route.queryParams);
// This if statement will pass on the second portion of the SSO flow
@@ -371,7 +367,7 @@ export class SsoComponent implements OnInit {
codeVerifier,
redirectUri,
orgSsoIdentifier,
- email,
+ email ?? undefined,
);
this.formPromise = this.loginStrategyService.logIn(credentials);
const authResult = await this.formPromise;
@@ -388,10 +384,10 @@ export class SsoComponent implements OnInit {
// - TDE login decryption options component
// - Browser SSO on extension open
// Note: you cannot set this in state before 2FA b/c there won't be an account in state.
- await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(
- orgSsoIdentifier,
- this.activeUserId,
- );
+
+ // Grabbing the active user id right before making the state set to ensure it exists.
+ const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
+ await this.ssoLoginService.setActiveUserOrganizationSsoIdentifier(orgSsoIdentifier, userId);
// Users enrolled in admin acct recovery can be forced to set a new password after
// having the admin set a temp password for them (affects TDE & standard users)
diff --git a/libs/common/spec/fake-account-service.ts b/libs/common/spec/fake-account-service.ts
index d45448ce698..ba48181faa2 100644
--- a/libs/common/spec/fake-account-service.ts
+++ b/libs/common/spec/fake-account-service.ts
@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { mock } from "jest-mock-extended";
-import { ReplaySubject, combineLatest, map } from "rxjs";
+import { ReplaySubject, combineLatest, map, Observable } from "rxjs";
import { Account, AccountInfo, AccountService } from "../src/auth/abstractions/account.service";
import { UserId } from "../src/types/guid";
@@ -55,7 +55,7 @@ export class FakeAccountService implements AccountService {
}),
);
}
- get nextUpAccount$() {
+ get nextUpAccount$(): Observable {
return combineLatest([this.accounts$, this.activeAccount$, this.sortedUserIds$]).pipe(
map(([accounts, activeAccount, sortedUserIds]) => {
const nextId = sortedUserIds.find((id) => id !== activeAccount?.id && accounts[id] != null);
diff --git a/libs/common/spec/fake-state-provider.ts b/libs/common/spec/fake-state-provider.ts
index b5105bb24ba..9f72ccada55 100644
--- a/libs/common/spec/fake-state-provider.ts
+++ b/libs/common/spec/fake-state-provider.ts
@@ -225,9 +225,9 @@ export class FakeStateProvider implements StateProvider {
async setUserState(
userKeyDefinition: UserKeyDefinition,
- value: T,
+ value: T | null,
userId?: UserId,
- ): Promise<[UserId, T]> {
+ ): Promise<[UserId, T | null]> {
await this.mock.setUserState(userKeyDefinition, value, userId);
if (userId) {
return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)];
diff --git a/libs/common/spec/fake-state.ts b/libs/common/spec/fake-state.ts
index e4b42e357b6..00b12de2eb1 100644
--- a/libs/common/spec/fake-state.ts
+++ b/libs/common/spec/fake-state.ts
@@ -131,9 +131,9 @@ export class FakeSingleUserState implements SingleUserState {
}
async update(
- configureState: (state: T, dependency: TCombine) => T,
+ configureState: (state: T | null, dependency: TCombine) => T | null,
options?: StateUpdateOptions,
- ): Promise {
+ ): Promise {
options = populateOptionsWithDefault(options);
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
const combinedDependencies =
@@ -206,9 +206,9 @@ export class FakeActiveUserState implements ActiveUserState {
}
async update(
- configureState: (state: T, dependency: TCombine) => T,
+ configureState: (state: T | null, dependency: TCombine) => T | null,
options?: StateUpdateOptions,
- ): Promise<[UserId, T]> {
+ ): Promise<[UserId, T | null]> {
options = populateOptionsWithDefault(options);
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
const combinedDependencies =
diff --git a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts
index bf64dcafd69..b30739d94a8 100644
--- a/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts
+++ b/libs/common/src/auth/abstractions/sso-login.service.abstraction.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { UserId } from "@bitwarden/common/types/guid";
export abstract class SsoLoginServiceAbstraction {
@@ -13,7 +11,7 @@ export abstract class SsoLoginServiceAbstraction {
* @see https://datatracker.ietf.org/doc/html/rfc7636
* @returns The code verifier used for SSO.
*/
- getCodeVerifier: () => Promise;
+ abstract getCodeVerifier: () => Promise;
/**
* Sets the code verifier used for SSO.
*
@@ -23,7 +21,7 @@ export abstract class SsoLoginServiceAbstraction {
* and verify it matches the one sent in the request for the `authorization_code`.
* @see https://datatracker.ietf.org/doc/html/rfc7636
*/
- setCodeVerifier: (codeVerifier: string) => Promise;
+ abstract setCodeVerifier: (codeVerifier: string) => Promise;
/**
* Gets the value of the SSO state.
*
@@ -33,7 +31,7 @@ export abstract class SsoLoginServiceAbstraction {
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
* @returns The SSO state.
*/
- getSsoState: () => Promise;
+ abstract getSsoState: () => Promise;
/**
* Sets the value of the SSO state.
*
@@ -42,7 +40,7 @@ export abstract class SsoLoginServiceAbstraction {
* returns the `state` in the callback and the client verifies that the value returned matches the value sent.
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
*/
- setSsoState: (ssoState: string) => Promise;
+ abstract setSsoState: (ssoState: string) => Promise;
/**
* Gets the value of the user's organization sso identifier.
*
@@ -50,20 +48,20 @@ export abstract class SsoLoginServiceAbstraction {
* Do not use this value outside of the SSO login flow.
* @returns The user's organization identifier.
*/
- getOrganizationSsoIdentifier: () => Promise;
+ abstract getOrganizationSsoIdentifier: () => Promise;
/**
* Sets the value of the user's organization sso identifier.
*
* This should only be used during the SSO flow to identify the organization that the user is attempting to log in to.
* Do not use this value outside of the SSO login flow.
*/
- setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise;
+ abstract setOrganizationSsoIdentifier: (organizationIdentifier: string) => Promise;
/**
* Gets the user's email.
* Note: This should only be used during the SSO flow to identify the user that is attempting to log in.
* @returns The user's email.
*/
- getSsoEmail: () => Promise;
+ abstract getSsoEmail: () => Promise;
/**
* Sets the user's email.
* Note: This should only be used during the SSO flow to identify the user that is attempting to log in.
@@ -71,20 +69,20 @@ export abstract class SsoLoginServiceAbstraction {
* @returns A promise that resolves when the email has been set.
*
*/
- setSsoEmail: (email: string) => Promise;
+ abstract setSsoEmail: (email: string) => Promise;
/**
* Gets the value of the active user's organization sso identifier.
*
* This should only be used post successful SSO login once the user is initialized.
* @param userId The user id for retrieving the org identifier state.
*/
- getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise;
+ abstract getActiveUserOrganizationSsoIdentifier: (userId: UserId) => Promise;
/**
* Sets the value of the active user's organization sso identifier.
*
* This should only be used post successful SSO login once the user is initialized.
*/
- setActiveUserOrganizationSsoIdentifier: (
+ abstract setActiveUserOrganizationSsoIdentifier: (
organizationIdentifier: string,
userId: UserId | undefined,
) => Promise;
diff --git a/libs/common/src/auth/services/auth.service.spec.ts b/libs/common/src/auth/services/auth.service.spec.ts
index 4b7b8a2b262..fc236c91a21 100644
--- a/libs/common/src/auth/services/auth.service.spec.ts
+++ b/libs/common/src/auth/services/auth.service.spec.ts
@@ -1,9 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import {
FakeAccountService,
makeStaticByteArray,
diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts
index d855a1be34f..da70baf3999 100644
--- a/libs/common/src/auth/services/auth.service.ts
+++ b/libs/common/src/auth/services/auth.service.ts
@@ -11,9 +11,8 @@ import {
switchMap,
} from "rxjs";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import { ApiService } from "../../abstractions/api.service";
import { StateService } from "../../platform/abstractions/state.service";
import { MessageSender } from "../../platform/messaging";
diff --git a/libs/common/src/auth/services/device-trust.service.implementation.ts b/libs/common/src/auth/services/device-trust.service.implementation.ts
index 903c72d4211..fe43df53c48 100644
--- a/libs/common/src/auth/services/device-trust.service.implementation.ts
+++ b/libs/common/src/auth/services/device-trust.service.implementation.ts
@@ -3,10 +3,8 @@
import { firstValueFrom, map, Observable } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
+import { KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { AppIdService } from "../../platform/abstractions/app-id.service";
import { ConfigService } from "../../platform/abstractions/config/config.service";
diff --git a/libs/common/src/auth/services/device-trust.service.spec.ts b/libs/common/src/auth/services/device-trust.service.spec.ts
index 06da1396074..e689c93395d 100644
--- a/libs/common/src/auth/services/device-trust.service.spec.ts
+++ b/libs/common/src/auth/services/device-trust.service.spec.ts
@@ -4,13 +4,11 @@ import { matches, mock } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom, of } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
+import { KeyService } from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider";
diff --git a/libs/common/src/auth/services/key-connector.service.spec.ts b/libs/common/src/auth/services/key-connector.service.spec.ts
index 165dcee1ea8..ec03c7ece55 100644
--- a/libs/common/src/auth/services/key-connector.service.spec.ts
+++ b/libs/common/src/auth/services/key-connector.service.spec.ts
@@ -2,10 +2,8 @@ import { mock } from "jest-mock-extended";
import { of } from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
+import { KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
import { ApiService } from "../../abstractions/api.service";
import { OrganizationData } from "../../admin-console/models/data/organization.data";
diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts
index 8953d14c8e6..8516400fe09 100644
--- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts
+++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.spec.ts
@@ -2,10 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
+import { KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
diff --git a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts
index c0a961d5bbb..824521d8a2e 100644
--- a/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts
+++ b/libs/common/src/auth/services/password-reset-enrollment.service.implementation.ts
@@ -6,10 +6,8 @@ import {
OrganizationUserApiService,
OrganizationUserResetPasswordEnrollmentRequest,
} from "@bitwarden/admin-console/common";
+import { KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "../../platform/abstractions/i18n.service";
diff --git a/libs/common/src/auth/services/sso-login.service.spec.ts b/libs/common/src/auth/services/sso-login.service.spec.ts
index 9cf49a07834..6764755e6ca 100644
--- a/libs/common/src/auth/services/sso-login.service.spec.ts
+++ b/libs/common/src/auth/services/sso-login.service.spec.ts
@@ -87,7 +87,7 @@ describe("SSOLoginService ", () => {
const orgIdentifier = "test-active-org-identifier";
await sut.setActiveUserOrganizationSsoIdentifier(orgIdentifier, undefined);
- expect(mockLogService.warning).toHaveBeenCalledWith(
+ expect(mockLogService.error).toHaveBeenCalledWith(
"Tried to set a user organization sso identifier with an undefined user id.",
);
});
diff --git a/libs/common/src/auth/services/sso-login.service.ts b/libs/common/src/auth/services/sso-login.service.ts
index c73be3630be..9b4b8656782 100644
--- a/libs/common/src/auth/services/sso-login.service.ts
+++ b/libs/common/src/auth/services/sso-login.service.ts
@@ -1,5 +1,3 @@
-// FIXME: Update this file to be type safe and remove this and next line
-// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@@ -75,7 +73,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
this.ssoEmailState = this.stateProvider.getGlobal(SSO_EMAIL);
}
- getCodeVerifier(): Promise {
+ getCodeVerifier(): Promise {
return firstValueFrom(this.codeVerifierState.state$);
}
@@ -83,7 +81,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
await this.codeVerifierState.update((_) => codeVerifier);
}
- getSsoState(): Promise {
+ getSsoState(): Promise {
return firstValueFrom(this.ssoState.state$);
}
@@ -91,7 +89,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
await this.ssoState.update((_) => ssoState);
}
- getOrganizationSsoIdentifier(): Promise {
+ getOrganizationSsoIdentifier(): Promise {
return firstValueFrom(this.orgSsoIdentifierState.state$);
}
@@ -99,7 +97,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
await this.orgSsoIdentifierState.update((_) => organizationIdentifier);
}
- getSsoEmail(): Promise {
+ getSsoEmail(): Promise {
return firstValueFrom(this.ssoEmailState.state$);
}
@@ -107,7 +105,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
await this.ssoEmailState.update((_) => email);
}
- getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise {
+ getActiveUserOrganizationSsoIdentifier(userId: UserId): Promise {
return firstValueFrom(this.userOrgSsoIdentifierState(userId).state$);
}
@@ -116,7 +114,7 @@ export class SsoLoginService implements SsoLoginServiceAbstraction {
userId: UserId | undefined,
): Promise {
if (userId === undefined) {
- this.logService.warning(
+ this.logService.error(
"Tried to set a user organization sso identifier with an undefined user id.",
);
return;
diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
index 102e4bac8da..677b6ff4499 100644
--- a/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
+++ b/libs/common/src/auth/services/user-verification/user-verification.service.spec.ts
@@ -12,11 +12,9 @@ import {
BiometricsStatus,
KdfConfig,
KeyService,
+ KdfConfigService,
} from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KdfConfigService } from "../../../../../key-management/src/abstractions/kdf-config.service";
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
import { VaultTimeoutSettingsService } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { I18nService } from "../../../platform/abstractions/i18n.service";
diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts
index 550a8c07ff7..a8e036c82d6 100644
--- a/libs/common/src/enums/feature-flag.enum.ts
+++ b/libs/common/src/enums/feature-flag.enum.ts
@@ -26,7 +26,6 @@ export enum FeatureFlag {
/* Tools */
ItemShare = "item-share",
- GeneratorToolsModernization = "generator-tools-modernization",
CriticalApps = "pm-14466-risk-insights-critical-application",
EnableRiskInsightsNotifications = "enable-risk-insights-notifications",
@@ -88,7 +87,6 @@ export const DefaultFeatureFlagValue = {
/* Tools */
[FeatureFlag.ItemShare]: FALSE,
- [FeatureFlag.GeneratorToolsModernization]: FALSE,
[FeatureFlag.CriticalApps]: FALSE,
[FeatureFlag.EnableRiskInsightsNotifications]: FALSE,
diff --git a/libs/common/src/key-management/services/default-process-reload.service.ts b/libs/common/src/key-management/services/default-process-reload.service.ts
index 9f97e0a94c1..09fe4e7e10c 100644
--- a/libs/common/src/key-management/services/default-process-reload.service.ts
+++ b/libs/common/src/key-management/services/default-process-reload.service.ts
@@ -2,11 +2,9 @@
// @ts-strict-ignore
import { firstValueFrom, map, timeout } from "rxjs";
+import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { BiometricStateService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountService } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service";
diff --git a/libs/common/src/platform/misc/utils.ts b/libs/common/src/platform/misc/utils.ts
index eaa8f0a813a..ef65d2130a0 100644
--- a/libs/common/src/platform/misc/utils.ts
+++ b/libs/common/src/platform/misc/utils.ts
@@ -8,9 +8,8 @@ import { Observable, of, switchMap } from "rxjs";
import { getHostname, parse } from "tldts";
import { Merge } from "type-fest";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { I18nService } from "../abstractions/i18n.service";
diff --git a/libs/common/src/platform/models/domain/enc-string.spec.ts b/libs/common/src/platform/models/domain/enc-string.spec.ts
index 9af19d36015..3b2586fc22f 100644
--- a/libs/common/src/platform/models/domain/enc-string.spec.ts
+++ b/libs/common/src/platform/models/domain/enc-string.spec.ts
@@ -1,8 +1,7 @@
import { mock, MockProxy } from "jest-mock-extended";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import { makeEncString, makeStaticByteArray } from "../../../../spec";
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts
index c6b330857a4..f0586e37ff7 100644
--- a/libs/common/src/platform/notifications/internal/default-notifications.service.ts
+++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts
@@ -151,11 +151,15 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
await this.syncService.syncUpsertCipher(
notification.payload as SyncCipherNotification,
notification.type === NotificationType.SyncCipherUpdate,
+ payloadUserId,
);
break;
case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete:
- await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification);
+ await this.syncService.syncDeleteCipher(
+ notification.payload as SyncCipherNotification,
+ payloadUserId,
+ );
break;
case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderUpdate:
diff --git a/libs/common/src/platform/services/container.service.ts b/libs/common/src/platform/services/container.service.ts
index 7421de8cc2c..1428b2bbd7c 100644
--- a/libs/common/src/platform/services/container.service.ts
+++ b/libs/common/src/platform/services/container.service.ts
@@ -1,6 +1,5 @@
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
export class ContainerService {
diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
index 226f4c2cfe9..3ea86a1f504 100644
--- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
+++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts
@@ -3,7 +3,8 @@ import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs";
-import { Account, AccountService } from "../../../auth/abstractions/account.service";
+import { mockAccountServiceWith } from "../../../../spec";
+import { Account } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
@@ -46,7 +47,6 @@ describe("FidoAuthenticatorService", () => {
let userInterface!: MockProxy>;
let userInterfaceSession!: MockProxy;
let syncService!: MockProxy;
- let accountService!: MockProxy;
let authenticator!: Fido2AuthenticatorService;
let windowReference!: ParentWindowReference;
@@ -58,7 +58,7 @@ describe("FidoAuthenticatorService", () => {
syncService = mock({
activeUserLastSync$: () => of(new Date()),
});
- accountService = mock();
+ const accountService = mockAccountServiceWith("testId" as UserId);
authenticator = new Fido2AuthenticatorService(
cipherService,
userInterface,
diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
index 376f4dcdced..76bd19b2876 100644
--- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
+++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts
@@ -1,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
-import { firstValueFrom, map } from "rxjs";
+import { firstValueFrom } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service";
+import { getUserId } from "../../../auth/services/account.service";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
@@ -145,10 +146,10 @@ export class Fido2AuthenticatorService
try {
keyPair = await createKeyPair();
pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
- const encrypted = await this.cipherService.get(cipherId);
const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ this.accountService.activeAccount$.pipe(getUserId),
);
+ const encrypted = await this.cipherService.get(cipherId, activeUserId);
cipher = await encrypted.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
@@ -309,7 +310,7 @@ export class Fido2AuthenticatorService
if (selectedFido2Credential.counter > 0) {
const activeUserId = await firstValueFrom(
- this.accountService.activeAccount$.pipe(map((a) => a?.id)),
+ this.accountService.activeAccount$.pipe(getUserId),
);
const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
await this.cipherService.updateWithServer(encrypted);
@@ -400,7 +401,8 @@ export class Fido2AuthenticatorService
return [];
}
- const ciphers = await this.cipherService.getAllDecrypted();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
return ciphers
.filter(
(cipher) =>
@@ -421,7 +423,8 @@ export class Fido2AuthenticatorService
return [];
}
- const ciphers = await this.cipherService.getAllDecrypted();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
return ciphers.filter(
(cipher) =>
!cipher.isDeleted &&
@@ -438,7 +441,8 @@ export class Fido2AuthenticatorService
}
private async findCredentialsByRp(rpId: string): Promise {
- const ciphers = await this.cipherService.getAllDecrypted();
+ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
+ const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
return ciphers.filter(
(cipher) =>
!cipher.isDeleted &&
diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts
index 16b3968045a..84511d1e71a 100644
--- a/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts
+++ b/libs/common/src/platform/services/user-auto-unlock-key.service.spec.ts
@@ -1,8 +1,7 @@
import { mock } from "jest-mock-extended";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { DefaultKeyService } from "../../../../key-management/src/key.service";
+import { DefaultKeyService } from "@bitwarden/key-management";
+
import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid";
import { UserKey } from "../../types/key";
diff --git a/libs/common/src/platform/services/user-auto-unlock-key.service.ts b/libs/common/src/platform/services/user-auto-unlock-key.service.ts
index a8947a49f45..bf64c13b060 100644
--- a/libs/common/src/platform/services/user-auto-unlock-key.service.ts
+++ b/libs/common/src/platform/services/user-auto-unlock-key.service.ts
@@ -1,6 +1,5 @@
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
+import { KeyService } from "@bitwarden/key-management";
+
import { UserId } from "../../types/guid";
import { KeySuffixOptions } from "../enums";
diff --git a/libs/common/src/platform/state/global-state.ts b/libs/common/src/platform/state/global-state.ts
index 82a6e2b348c..b2ac634df24 100644
--- a/libs/common/src/platform/state/global-state.ts
+++ b/libs/common/src/platform/state/global-state.ts
@@ -18,13 +18,13 @@ export interface GlobalState {
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
update: (
- configureState: (state: T, dependency: TCombine) => T,
+ configureState: (state: T | null, dependency: TCombine) => T | null,
options?: StateUpdateOptions,
- ) => Promise;
+ ) => Promise;
/**
* An observable stream of this state, the first emission of this will be the current state on disk
* and subsequent updates will be from an update to that state.
*/
- state$: Observable;
+ state$: Observable;
}
diff --git a/libs/common/src/platform/state/implementations/default-single-user-state.ts b/libs/common/src/platform/state/implementations/default-single-user-state.ts
index 4cfba1ffa95..1dafd3aecad 100644
--- a/libs/common/src/platform/state/implementations/default-single-user-state.ts
+++ b/libs/common/src/platform/state/implementations/default-single-user-state.ts
@@ -16,7 +16,7 @@ export class DefaultSingleUserState
extends StateBase>
implements SingleUserState
{
- readonly combinedState$: Observable>;
+ readonly combinedState$: Observable>;
constructor(
readonly userId: UserId,
diff --git a/libs/common/src/platform/state/implementations/default-state.provider.ts b/libs/common/src/platform/state/implementations/default-state.provider.ts
index f86ba11b268..31795767979 100644
--- a/libs/common/src/platform/state/implementations/default-state.provider.ts
+++ b/libs/common/src/platform/state/implementations/default-state.provider.ts
@@ -54,9 +54,9 @@ export class DefaultStateProvider implements StateProvider {
async setUserState(
userKeyDefinition: UserKeyDefinition,
- value: T,
+ value: T | null,
userId?: UserId,
- ): Promise<[UserId, T]> {
+ ): Promise<[UserId, T | null]> {
if (userId) {
return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)];
} else {
diff --git a/libs/common/src/platform/state/implementations/state-base.ts b/libs/common/src/platform/state/implementations/state-base.ts
index f285963d6e6..578720a2281 100644
--- a/libs/common/src/platform/state/implementations/state-base.ts
+++ b/libs/common/src/platform/state/implementations/state-base.ts
@@ -1,12 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import {
- Observable,
- ReplaySubject,
defer,
filter,
firstValueFrom,
merge,
+ Observable,
+ ReplaySubject,
share,
switchMap,
tap,
@@ -22,7 +22,7 @@ import {
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DebugOptions } from "../key-definition";
-import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
+import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options";
import { getStoredValue } from "./util";
@@ -36,7 +36,7 @@ type KeyDefinitionRequirements = {
export abstract class StateBase> {
private updatePromise: Promise;
- readonly state$: Observable;
+ readonly state$: Observable;
constructor(
protected readonly key: StorageKey,
@@ -86,9 +86,9 @@ export abstract class StateBase>
}
async update(
- configureState: (state: T, dependency: TCombine) => T,
+ configureState: (state: T | null, dependency: TCombine) => T | null,
options: StateUpdateOptions = {},
- ): Promise {
+ ): Promise {
options = populateOptionsWithDefault(options);
if (this.updatePromise != null) {
await this.updatePromise;
@@ -96,17 +96,16 @@ export abstract class StateBase>
try {
this.updatePromise = this.internalUpdate(configureState, options);
- const newState = await this.updatePromise;
- return newState;
+ return await this.updatePromise;
} finally {
this.updatePromise = null;
}
}
private async internalUpdate(
- configureState: (state: T, dependency: TCombine) => T,
+ configureState: (state: T | null, dependency: TCombine) => T | null,
options: StateUpdateOptions,
- ): Promise {
+ ): Promise {
const currentState = await this.getStateForUpdate();
const combinedDependencies =
options.combineLatestWith != null
@@ -122,7 +121,7 @@ export abstract class StateBase>
return newState;
}
- protected async doStorageSave(newState: T, oldState: T) {
+ protected async doStorageSave(newState: T | null, oldState: T) {
if (this.keyDefinition.debug.enableUpdateLogging) {
this.logService.info(
`Updating '${this.key}' from ${oldState == null ? "null" : "non-null"} to ${newState == null ? "null" : "non-null"}`,
diff --git a/libs/common/src/platform/state/state.provider.ts b/libs/common/src/platform/state/state.provider.ts
index 44736500afc..dc8cb3e9359 100644
--- a/libs/common/src/platform/state/state.provider.ts
+++ b/libs/common/src/platform/state/state.provider.ts
@@ -60,9 +60,9 @@ export abstract class StateProvider {
*/
abstract setUserState(
keyDefinition: UserKeyDefinition,
- value: T,
+ value: T | null,
userId?: UserId,
- ): Promise<[UserId, T]>;
+ ): Promise<[UserId, T | null]>;
/** @see{@link ActiveUserStateProvider.get} */
abstract getActive(userKeyDefinition: UserKeyDefinition): ActiveUserState;
diff --git a/libs/common/src/platform/state/user-state.ts b/libs/common/src/platform/state/user-state.ts
index 22c255eb985..26fa6f83fa3 100644
--- a/libs/common/src/platform/state/user-state.ts
+++ b/libs/common/src/platform/state/user-state.ts
@@ -12,7 +12,7 @@ export interface UserState {
readonly state$: Observable;
/** Emits a stream of tuples, with the first element being a user id and the second element being the data for that user. */
- readonly combinedState$: Observable>;
+ readonly combinedState$: Observable>;
}
export const activeMarker: unique symbol = Symbol("active");
@@ -38,9 +38,9 @@ export interface ActiveUserState extends UserState {
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
readonly update: (
- configureState: (state: T, dependencies: TCombine) => T,
+ configureState: (state: T | null, dependencies: TCombine) => T | null,
options?: StateUpdateOptions,
- ) => Promise<[UserId, T]>;
+ ) => Promise<[UserId, T | null]>;
}
export interface SingleUserState extends UserState {
@@ -58,7 +58,7 @@ export interface SingleUserState extends UserState {
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
*/
readonly update: (
- configureState: (state: T, dependencies: TCombine) => T,
+ configureState: (state: T | null, dependencies: TCombine) => T | null,
options?: StateUpdateOptions,
- ) => Promise;
+ ) => Promise;
}
diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts
index cfa9030c9de..92a10baf6d2 100644
--- a/libs/common/src/platform/sync/core-sync.service.ts
+++ b/libs/common/src/platform/sync/core-sync.service.ts
@@ -129,12 +129,18 @@ export abstract class CoreSyncService implements SyncService {
return this.syncCompleted(false);
}
- async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise {
+ async syncUpsertCipher(
+ notification: SyncCipherNotification,
+ isEdit: boolean,
+ userId: UserId,
+ ): Promise {
this.syncStarted();
- if (await this.stateService.getIsAuthenticated()) {
+
+ const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
+ if (authStatus >= AuthenticationStatus.Locked) {
try {
let shouldUpdate = true;
- const localCipher = await this.cipherService.get(notification.id);
+ const localCipher = await this.cipherService.get(notification.id, userId);
if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
shouldUpdate = false;
}
@@ -182,7 +188,7 @@ export abstract class CoreSyncService implements SyncService {
}
} catch (e) {
if (e != null && e.statusCode === 404 && isEdit) {
- await this.cipherService.delete(notification.id);
+ await this.cipherService.delete(notification.id, userId);
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true);
}
@@ -191,10 +197,12 @@ export abstract class CoreSyncService implements SyncService {
return this.syncCompleted(false);
}
- async syncDeleteCipher(notification: SyncCipherNotification): Promise {
+ async syncDeleteCipher(notification: SyncCipherNotification, userId: UserId): Promise {
this.syncStarted();
- if (await this.stateService.getIsAuthenticated()) {
- await this.cipherService.delete(notification.id);
+
+ const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
+ if (authStatus >= AuthenticationStatus.Locked) {
+ await this.cipherService.delete(notification.id, userId);
this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true);
}
diff --git a/libs/common/src/platform/sync/default-sync.service.ts b/libs/common/src/platform/sync/default-sync.service.ts
index 982be453457..b47f0c54208 100644
--- a/libs/common/src/platform/sync/default-sync.service.ts
+++ b/libs/common/src/platform/sync/default-sync.service.ts
@@ -7,6 +7,7 @@ import {
CollectionData,
CollectionDetailsResponse,
} from "@bitwarden/admin-console/common";
+import { KeyService } from "@bitwarden/key-management";
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
@@ -14,9 +15,6 @@ import { UserDecryptionOptionsServiceAbstraction } from "../../../../auth/src/co
// FIXME: remove `src` and fix import
// eslint-disable-next-line no-restricted-imports
import { LogoutReason } from "../../../../auth/src/common/types";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { ApiService } from "../../abstractions/api.service";
import { InternalOrganizationServiceAbstraction } from "../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts
index 6763e01cab7..967e4db27a5 100644
--- a/libs/common/src/platform/sync/sync.service.ts
+++ b/libs/common/src/platform/sync/sync.service.ts
@@ -62,8 +62,9 @@ export abstract class SyncService {
abstract syncUpsertCipher(
notification: SyncCipherNotification,
isEdit: boolean,
+ userId: UserId,
): Promise;
- abstract syncDeleteCipher(notification: SyncFolderNotification): Promise;
+ abstract syncDeleteCipher(notification: SyncFolderNotification, userId: UserId): Promise;
abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise;
abstract syncDeleteSend(notification: SyncSendNotification): Promise;
}
diff --git a/libs/common/src/platform/theming/theme-state.service.ts b/libs/common/src/platform/theming/theme-state.service.ts
index bb146be4927..df2c96c49d0 100644
--- a/libs/common/src/platform/theming/theme-state.service.ts
+++ b/libs/common/src/platform/theming/theme-state.service.ts
@@ -32,7 +32,11 @@ export class DefaultThemeStateService implements ThemeStateService {
map(([theme, isExtensionRefresh]) => {
// The extension refresh should not allow for Nord or SolarizedDark
// Default the user to their system theme
- if (isExtensionRefresh && [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)) {
+ if (
+ isExtensionRefresh &&
+ theme != null &&
+ [ThemeType.Nord, ThemeType.SolarizedDark].includes(theme)
+ ) {
return ThemeType.System;
}
diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts
index da38ca5bfff..b37ec0de271 100644
--- a/libs/common/src/services/event/event-collection.service.ts
+++ b/libs/common/src/services/event/event-collection.service.ts
@@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
+import { UserId } from "@bitwarden/common/types/guid";
import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
import { EventUploadService } from "../../abstractions/event/event-upload.service";
@@ -46,7 +47,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
- if (!(await this.shouldUpdate(null, eventType, ciphers))) {
+ if (!(await this.shouldUpdate(userId, null, eventType, ciphers))) {
return;
}
@@ -91,7 +92,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
- if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) {
+ if (!(await this.shouldUpdate(userId, organizationId, eventType, undefined, cipherId))) {
return;
}
@@ -113,18 +114,18 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
}
/** Verifies if the event collection should be updated for the provided information
+ * @param userId the active user's id
* @param cipherId the cipher for the event
* @param organizationId the organization for the event
*/
private async shouldUpdate(
+ userId: UserId,
organizationId: string = null,
eventType: EventType = null,
ciphers: CipherView[] = [],
cipherId?: string,
): Promise {
- const cipher$ = from(this.cipherService.get(cipherId));
-
- const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
+ const cipher$ = from(this.cipherService.get(cipherId, userId));
const orgIds$ = this.organizationService
.organizations$(userId)
diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts
index 77ed6c960ab..78c0bc43331 100644
--- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts
+++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts
@@ -6,11 +6,8 @@ import {
FakeUserDecryptionOptions as UserDecryptionOptions,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
-import { BiometricStateService } from "@bitwarden/key-management";
+import { BiometricStateService, KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { FakeAccountService, mockAccountServiceWith, FakeStateProvider } from "../../../spec";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts
index ffc8b6e0144..813f187043b 100644
--- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts
+++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts
@@ -19,11 +19,8 @@ import {
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
-import { BiometricStateService } from "@bitwarden/key-management";
+import { BiometricStateService, KeyService } from "@bitwarden/key-management";
-// FIXME: remove `src` and fix import
-// eslint-disable-next-line no-restricted-imports
-import { KeyService } from "../../../../key-management/src/abstractions/key.service";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "../../admin-console/enums";
diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts
index 169de447f10..b409f52d936 100644
--- a/libs/common/src/state-migrations/migrate.ts
+++ b/libs/common/src/state-migrations/migrate.ts
@@ -68,12 +68,13 @@ import { RemoveUnassignedItemsBannerDismissed } from "./migrations/67-remove-una
import { MoveLastSyncDate } from "./migrations/68-move-last-sync-date";
import { MigrateIncorrectFolderKey } from "./migrations/69-migrate-incorrect-folder-key";
import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account";
+import { RemoveAcBannersDismissed } from "./migrations/70-remove-ac-banner-dismissed";
import { MoveStateVersionMigrator } from "./migrations/8-move-state-version";
import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-settings-to-global";
import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 3;
-export const CURRENT_VERSION = 69;
+export const CURRENT_VERSION = 70;
export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() {
@@ -144,7 +145,8 @@ export function createMigrationBuilder() {
.with(MoveFinalDesktopSettingsMigrator, 65, 66)
.with(RemoveUnassignedItemsBannerDismissed, 66, 67)
.with(MoveLastSyncDate, 67, 68)
- .with(MigrateIncorrectFolderKey, 68, CURRENT_VERSION);
+ .with(MigrateIncorrectFolderKey, 68, 69)
+ .with(RemoveAcBannersDismissed, 69, CURRENT_VERSION);
}
export async function currentVersion(
diff --git a/libs/common/src/tools/log/default-semantic-logger.spec.ts b/libs/common/src/tools/log/default-semantic-logger.spec.ts
new file mode 100644
index 00000000000..8d1dadb66af
--- /dev/null
+++ b/libs/common/src/tools/log/default-semantic-logger.spec.ts
@@ -0,0 +1,199 @@
+import { mock } from "jest-mock-extended";
+
+import { LogService } from "../../platform/abstractions/log.service";
+import { LogLevelType } from "../../platform/enums";
+
+import { DefaultSemanticLogger } from "./default-semantic-logger";
+
+const logger = mock();
+
+describe("DefaultSemanticLogger", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe("debug", () => {
+ it("writes structural log messages to console.log", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.debug("this is a debug message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
+ message: "this is a debug message",
+ level: LogLevelType.Debug,
+ });
+ });
+
+ it("writes structural content to console.log", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.debug({ example: "this is content" });
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, {
+ content: { example: "this is content" },
+ level: LogLevelType.Debug,
+ });
+ });
+
+ it("writes structural content to console.log with a message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.info({ example: "this is content" }, "this is a message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
+ content: { example: "this is content" },
+ message: "this is a message",
+ level: LogLevelType.Info,
+ });
+ });
+ });
+
+ describe("info", () => {
+ it("writes structural log messages to console.log", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.info("this is an info message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
+ message: "this is an info message",
+ level: LogLevelType.Info,
+ });
+ });
+
+ it("writes structural content to console.log", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.info({ example: "this is content" });
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
+ content: { example: "this is content" },
+ level: LogLevelType.Info,
+ });
+ });
+
+ it("writes structural content to console.log with a message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.info({ example: "this is content" }, "this is a message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, {
+ content: { example: "this is content" },
+ message: "this is a message",
+ level: LogLevelType.Info,
+ });
+ });
+ });
+
+ describe("warn", () => {
+ it("writes structural log messages to console.warn", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.warn("this is a warning message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
+ message: "this is a warning message",
+ level: LogLevelType.Warning,
+ });
+ });
+
+ it("writes structural content to console.warn", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.warn({ example: "this is content" });
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
+ content: { example: "this is content" },
+ level: LogLevelType.Warning,
+ });
+ });
+
+ it("writes structural content to console.warn with a message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.warn({ example: "this is content" }, "this is a message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, {
+ content: { example: "this is content" },
+ message: "this is a message",
+ level: LogLevelType.Warning,
+ });
+ });
+ });
+
+ describe("error", () => {
+ it("writes structural log messages to console.error", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.error("this is an error message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ message: "this is an error message",
+ level: LogLevelType.Error,
+ });
+ });
+
+ it("writes structural content to console.error", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.error({ example: "this is content" });
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ content: { example: "this is content" },
+ level: LogLevelType.Error,
+ });
+ });
+
+ it("writes structural content to console.error with a message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ log.error({ example: "this is content" }, "this is a message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ content: { example: "this is content" },
+ message: "this is a message",
+ level: LogLevelType.Error,
+ });
+ });
+ });
+
+ describe("panic", () => {
+ it("writes structural log messages to console.error before throwing the message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ expect(() => log.panic("this is an error message")).toThrow("this is an error message");
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ message: "this is an error message",
+ level: LogLevelType.Error,
+ });
+ });
+
+ it("writes structural log messages to console.error with a message before throwing the message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ expect(() => log.panic({ example: "this is content" }, "this is an error message")).toThrow(
+ "this is an error message",
+ );
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ content: { example: "this is content" },
+ message: "this is an error message",
+ level: LogLevelType.Error,
+ });
+ });
+
+ it("writes structural log messages to console.error with a content before throwing the message", () => {
+ const log = new DefaultSemanticLogger(logger, {});
+
+ expect(() => log.panic("this is content", "this is an error message")).toThrow(
+ "this is an error message",
+ );
+
+ expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, {
+ content: "this is content",
+ message: "this is an error message",
+ level: LogLevelType.Error,
+ });
+ });
+ });
+});
diff --git a/libs/common/src/tools/log/default-semantic-logger.ts b/libs/common/src/tools/log/default-semantic-logger.ts
new file mode 100644
index 00000000000..1bd62baf126
--- /dev/null
+++ b/libs/common/src/tools/log/default-semantic-logger.ts
@@ -0,0 +1,65 @@
+import { Jsonify } from "type-fest";
+
+import { LogService } from "../../platform/abstractions/log.service";
+import { LogLevelType } from "../../platform/enums";
+
+import { SemanticLogger } from "./semantic-logger.abstraction";
+
+/** Sends semantic logs to the console.
+ * @remarks the behavior of this logger is based on `LogService`; it
+ * replaces dynamic messages (`%s`) with a JSON-formatted semantic log.
+ */
+export class DefaultSemanticLogger implements SemanticLogger {
+ /** Instantiates a console semantic logger
+ * @param context a static payload that is cloned when the logger
+ * logs a message. The `messages`, `level`, and `content` fields
+ * are reserved for use by loggers.
+ */
+ constructor(
+ private logger: LogService,
+ context: Jsonify,
+ ) {
+ this.context = context && typeof context === "object" ? context : {};
+ }
+
+ readonly context: object;
+
+ debug(content: Jsonify, message?: string): void {
+ this.log(content, LogLevelType.Debug, message);
+ }
+
+ info(content: Jsonify, message?: string): void {
+ this.log(content, LogLevelType.Info, message);
+ }
+
+ warn(content: Jsonify, message?: string): void {
+ this.log(content, LogLevelType.Warning, message);
+ }
+
+ error(content: Jsonify, message?: string): void {
+ this.log(content, LogLevelType.Error, message);
+ }
+
+ panic(content: Jsonify, message?: string): never {
+ this.log(content, LogLevelType.Error, message);
+ const panicMessage =
+ message ?? (typeof content === "string" ? content : "a fatal error occurred");
+ throw new Error(panicMessage);
+ }
+
+ private log(content: Jsonify, level: LogLevelType, message?: string) {
+ const log = {
+ ...this.context,
+ message,
+ content: content ?? undefined,
+ level,
+ };
+
+ if (typeof content === "string" && !message) {
+ log.message = content;
+ delete log.content;
+ }
+
+ this.logger.write(level, log);
+ }
+}
diff --git a/libs/common/src/tools/log/disabled-semantic-logger.ts b/libs/common/src/tools/log/disabled-semantic-logger.ts
new file mode 100644
index 00000000000..054c3ed390b
--- /dev/null
+++ b/libs/common/src/tools/log/disabled-semantic-logger.ts
@@ -0,0 +1,18 @@
+import { Jsonify } from "type-fest";
+
+import { SemanticLogger } from "./semantic-logger.abstraction";
+
+/** Disables semantic logs. Still panics. */
+export class DisabledSemanticLogger implements SemanticLogger {
+ debug(_content: Jsonify, _message?: string): void {}
+
+ info(_content: Jsonify, _message?: string): void {}
+
+ warn(_content: Jsonify, _message?: string): void {}
+
+ error(_content: Jsonify, _message?: string): void {}
+
+ panic(_content: Jsonify, message?: string): never {
+ throw new Error(message);
+ }
+}
diff --git a/libs/common/src/tools/log/factory.ts b/libs/common/src/tools/log/factory.ts
new file mode 100644
index 00000000000..d887c4e310a
--- /dev/null
+++ b/libs/common/src/tools/log/factory.ts
@@ -0,0 +1,30 @@
+import { Jsonify } from "type-fest";
+
+import { LogService } from "../../platform/abstractions/log.service";
+
+import { DefaultSemanticLogger } from "./default-semantic-logger";
+import { DisabledSemanticLogger } from "./disabled-semantic-logger";
+import { SemanticLogger } from "./semantic-logger.abstraction";
+
+/** Instantiates a semantic logger that emits nothing when a message
+ * is logged.
+ * @param _context a static payload that is cloned when the logger
+ * logs a message. The `messages`, `level`, and `content` fields
+ * are reserved for use by loggers.
+ */
+export function disabledSemanticLoggerProvider(
+ _context: Jsonify,
+): SemanticLogger {
+ return new DisabledSemanticLogger();
+}
+
+/** Instantiates a semantic logger that emits logs to the console.
+ * @param context a static payload that is cloned when the logger
+ * logs a message. The `messages`, `level`, and `content` fields
+ * are reserved for use by loggers.
+ * @param settings specializes how the semantic logger functions.
+ * If this is omitted, the logger suppresses debug messages.
+ */
+export function consoleSemanticLoggerProvider(logger: LogService): SemanticLogger {
+ return new DefaultSemanticLogger(logger, {});
+}
diff --git a/libs/common/src/tools/log/index.ts b/libs/common/src/tools/log/index.ts
new file mode 100644
index 00000000000..1f86ca4e4d7
--- /dev/null
+++ b/libs/common/src/tools/log/index.ts
@@ -0,0 +1,2 @@
+export { disabledSemanticLoggerProvider, consoleSemanticLoggerProvider } from "./factory";
+export { SemanticLogger } from "./semantic-logger.abstraction";
diff --git a/libs/common/src/tools/log/semantic-logger.abstraction.ts b/libs/common/src/tools/log/semantic-logger.abstraction.ts
new file mode 100644
index 00000000000..196d1f3f12c
--- /dev/null
+++ b/libs/common/src/tools/log/semantic-logger.abstraction.ts
@@ -0,0 +1,94 @@
+import { Jsonify } from "type-fest";
+
+/** Semantic/structural logging component */
+export interface SemanticLogger {
+ /** Logs a message at debug priority.
+ * Debug messages are used for diagnostics, and are typically disabled
+ * in production builds.
+ * @param message - a message to record in the log's `message` field.
+ */
+ debug(message: string): void;
+
+ /** Logs the content at debug priority.
+ * Debug messages are used for diagnostics, and are typically disabled
+ * in production builds.
+ * @param content - JSON content included in the log's `content` field.
+ * @param message - a message to record in the log's `message` field.
+ */
+ debug(content: Jsonify, message?: string): void;
+
+ /** combined signature for overloaded methods */
+ debug(content: Jsonify | string, message?: string): void;
+
+ /** Logs a message at informational priority.
+ * Information messages are used for status reports.
+ * @param message - a message to record in the log's `message` field.
+ */
+ info(message: string): void;
+
+ /** Logs the content at informational priority.
+ * Information messages are used for status reports.
+ * @param content - JSON content included in the log's `content` field.
+ * @param message - a message to record in the log's `message` field.
+ */
+ info(content: Jsonify, message?: string): void;
+
+ /** combined signature for overloaded methods */
+ info(content: Jsonify | string, message?: string): void;
+
+ /** Logs a message at warn priority.
+ * Warn messages are used to indicate a operation that may affect system
+ * stability occurred.
+ * @param message - a message to record in the log's `message` field.
+ */
+ warn(message: string): void;
+
+ /** Logs the content at warn priority.
+ * Warn messages are used to indicate a operation that may affect system
+ * stability occurred.
+ * @param content - JSON content included in the log's `content` field.
+ * @param message - a message to record in the log's `message` field.
+ */
+ warn(content: Jsonify, message?: string): void;
+
+ /** combined signature for overloaded methods */
+ warn(content: Jsonify | string, message?: string): void;
+
+ /** Logs a message at error priority.
+ * Error messages are used to indicate a operation that affects system
+ * stability occurred and the system was able to recover.
+ * @param message - a message to record in the log's `message` field.
+ */
+ error(message: string): void;
+
+ /** Logs the content at debug priority.
+ * Error messages are used to indicate a operation that affects system
+ * stability occurred and the system was able to recover.
+ * @param content - JSON content included in the log's `content` field.
+ * @param message - a message to record in the log's `message` field.
+ */
+ error(content: Jsonify, message?: string): void;
+
+ /** combined signature for overloaded methods */
+ error(content: Jsonify | string, message?: string): void;
+
+ /** Logs a message at panic priority and throws an error.
+ * Panic messages are used to indicate a operation that affects system
+ * stability occurred and the system cannot recover. Panic messages
+ * log an error and throw an `Error`.
+ * @param message - a message to record in the log's `message` field.
+ */
+ panic(message: string): never;
+
+ /** Logs the content at debug priority and throws an error.
+ * Panic messages are used to indicate a operation that affects system
+ * stability occurred and the system cannot recover. Panic messages
+ * log an error and throw an `Error`.
+ * @param content - JSON content included in the log's `content` field.
+ * @param message - a message to record in the log's `message` field.
+ */
+ panic(content: Jsonify, message?: string): never;
+
+ /** combined signature for overloaded methods */
+ panic(content: Jsonify | string, message?: string): never;
+}
diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts
index 0672ae29e91..1e4275ff89b 100644
--- a/libs/common/src/vault/abstractions/cipher.service.ts
+++ b/libs/common/src/vault/abstractions/cipher.service.ts
@@ -19,57 +19,70 @@ import { FieldView } from "../models/view/field.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
export abstract class CipherService implements UserKeyRotationDataProvider {
- cipherViews$: Observable;
- ciphers$: Observable>;
- localData$: Observable>;
+ abstract cipherViews$(userId: UserId): Observable;
+ abstract ciphers$(userId: UserId): Observable>;
+ abstract localData$(userId: UserId): Observable>;
/**
* An observable monitoring the add/edit cipher info saved to memory.
*/
- addEditCipherInfo$: Observable;
+ abstract addEditCipherInfo$(userId: UserId): Observable;
/**
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed.
*
* An empty array indicates that all ciphers were successfully decrypted.
*/
- failedToDecryptCiphers$: Observable;
- clearCache: (userId?: string) => Promise;
- encrypt: (
+ abstract failedToDecryptCiphers$(userId: UserId): Observable;
+ abstract clearCache(userId: UserId): Promise;
+ abstract encrypt(
model: CipherView,
userId: UserId,
keyForEncryption?: SymmetricCryptoKey,
keyForCipherKeyDecryption?: SymmetricCryptoKey,
originalCipher?: Cipher,
- ) => Promise;
- encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise;
- encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise;
- get: (id: string) => Promise;
- getAll: () => Promise;
- getAllDecrypted: () => Promise;
- getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise;
- getAllDecryptedForUrl: (
+ ): Promise