1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-11 22:13:32 +00:00

Merge branch 'main' into tools/generator/organize-types-and-data

This commit is contained in:
✨ Audrey ✨
2025-01-06 15:36:08 -05:00
283 changed files with 8988 additions and 13084 deletions

View File

@@ -114,8 +114,8 @@ jobs:
fi
build:
name: Build
build-source:
name: Build browser source
runs-on: ubuntu-22.04
needs:
- setup
@@ -127,7 +127,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@@ -169,21 +169,91 @@ jobs:
zip -r browser-source.zip browser-source
- name: Upload browser source
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: browser-source-${{ env._BUILD_NUMBER }}.zip
path: browser-source.zip
if-no-files-found: error
build:
name: Build
runs-on: ubuntu-22.04
needs:
- setup
- locales-test
- build-source
env:
_BUILD_NUMBER: ${{ needs.setup.outputs.adj_build_number }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
strategy:
matrix:
include:
- name: "chrome"
npm_command: "dist:chrome"
archive_name: "dist-chrome.zip"
artifact_name: "dist-chrome-MV3"
- name: "edge"
npm_command: "dist:edge"
archive_name: "dist-edge.zip"
artifact_name: "dist-edge"
- name: "edge-mv3"
npm_command: "dist:edge:mv3"
archive_name: "dist-edge.zip"
artifact_name: "DO-NOT-USE-FOR-PROD-dist-edge-MV3"
- 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"
- name: "opera-mv3"
npm_command: "dist:opera:mv3"
archive_name: "dist-opera.zip"
artifact_name: "DO-NOT-USE-FOR-PROD-dist-opera-MV3"
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Print environment
run: |
node --version
npm --version
- name: Download browser source
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: browser-source-${{ env._BUILD_NUMBER }}.zip
- name: Unzip browser source artifact
run: |
unzip browser-source.zip
rm browser-source.zip
- name: NPM setup
run: npm ci
working-directory: browser-source/
- name: Download SDK Artifacts
- name: Download SDK artifacts
if: ${{ inputs.sdk_branch != '' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
github_token: ${{ secrets.GITHUB_TOKEN }}
workflow: build-wasm-internal.yml
workflow_conclusion: success
branch: ${{ inputs.sdk_branch }}
@@ -195,85 +265,19 @@ jobs:
- name: Override SDK
if: ${{ inputs.sdk_branch != '' }}
working-directory: browser-source/
run: |
npm link ../sdk-internal
run: npm link ../sdk-internal
- name: Build Chrome
run: npm run dist:chrome
- name: Build extension
run: npm run ${{ matrix.npm_command }}
working-directory: browser-source/apps/browser
- name: Upload Chrome MV3 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
- name: Upload extension artifact
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: dist-chrome-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-chrome.zip
name: ${{ matrix.artifact_name }}-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/${{ matrix.archive_name }}
if-no-files-found: error
- name: Build Edge
run: npm run dist:edge
working-directory: browser-source/apps/browser
- name: Upload Edge artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-edge-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge.zip
if-no-files-found: error
- name: Build Edge (MV3)
run: npm run dist:edge:mv3
working-directory: browser-source/apps/browser
- name: Upload Edge MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-edge-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-edge.zip
if-no-files-found: error
- name: Build Firefox
run: npm run dist:firefox
working-directory: browser-source/apps/browser
- name: Upload Firefox artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-firefox-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox.zip
if-no-files-found: error
- name: Build Firefox (MV3)
run: npm run dist:firefox:mv3
working-directory: browser-source/apps/browser
- name: Upload Firefox MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-firefox-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-firefox.zip
if-no-files-found: error
- name: Build Opera
run: npm run dist:opera
working-directory: browser-source/apps/browser
- name: Upload Opera artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: dist-opera-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera.zip
if-no-files-found: error
- name: Build Opera (MV3)
run: npm run dist:opera:mv3
working-directory: browser-source/apps/browser
- name: Upload Opera MV3 artifact (DO NOT USE FOR PROD)
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: DO-NOT-USE-FOR-PROD-dist-opera-MV3-${{ env._BUILD_NUMBER }}.zip
path: browser-source/apps/browser/dist/dist-opera.zip
if-no-files-found: error
build-safari:
name: Build Safari
@@ -405,7 +409,7 @@ jobs:
ls -la
- name: Upload Safari artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: dist-safari-${{ env._BUILD_NUMBER }}.zip
path: apps/browser/dist/dist-safari.zip
@@ -448,6 +452,7 @@ jobs:
upload_sources: true
upload_translations: false
check-failures:
name: Check for failures
if: always()
@@ -455,6 +460,7 @@ jobs:
needs:
- setup
- locales-test
- build-source
- build
- build-safari
- crowdin-push

View File

@@ -163,14 +163,14 @@ jobs:
matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
- name: Upload unix zip asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload unix checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-${{ env.LOWER_RUNNER_OS }}-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -324,14 +324,14 @@ jobs:
-t sha256 | Out-File -Encoding ASCII ./dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${env:_PACKAGE_VERSION}.txt
- name: Upload windows zip asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-${{ env._PACKAGE_VERSION }}.zip
if-no-files-found: error
- name: Upload windows checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/bw${{ matrix.license_type.artifact_prefix }}-windows-sha256-${{ env._PACKAGE_VERSION }}.txt
@@ -339,7 +339,7 @@ jobs:
- name: Upload Chocolatey asset
if: matrix.license_type.build_prefix == 'bit'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/cli/dist/chocolatey/bitwarden-cli.${{ env._PACKAGE_VERSION }}.nupkg
@@ -350,7 +350,7 @@ jobs:
- name: Upload NPM Build Directory asset
if: matrix.license_type.build_prefix == 'bit'
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
path: apps/cli/bitwarden-cli-${{ env._PACKAGE_VERSION }}-npm-build.zip
@@ -421,14 +421,14 @@ jobs:
run: sudo snap remove bw
- name: Upload snap asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/cli/dist/snap/bw_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload snap checksum asset
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt
path: apps/cli/dist/snap/bw-snap-sha256-${{ env._PACKAGE_VERSION }}.txt

View File

@@ -207,7 +207,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -232,42 +232,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-linux.yml
@@ -280,7 +280,7 @@ jobs:
sudo npm run pack:lin:flatpak
- name: Upload flatpak artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: com.bitwarden.desktop.flatpak
path: apps/desktop/dist/com.bitwarden.desktop.flatpak
@@ -373,7 +373,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -428,91 +428,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release_channel }}.yml
@@ -561,14 +561,14 @@ jobs:
- name: Cache Build
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Cache Safari
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -681,7 +681,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -749,14 +749,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -869,7 +869,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -918,28 +918,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release_channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release_channel }}-mac.yml
@@ -990,14 +990,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -1117,7 +1117,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -1166,7 +1166,7 @@ jobs:
run: npm run pack:mac:mas
- name: Upload .pkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
@@ -1193,7 +1193,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc-desktop')
uses: slackapi/slack-github-action@37ebaef184d7626c5f204ab8d3baff4262dd30f0 # v1.27.0
uses: slackapi/slack-github-action@485a9d42d3a73031f12ec201c457e2162c45d02d # v2.0.0
with:
channel-id: C074F5UESQ0
payload: |
@@ -1252,14 +1252,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -1372,7 +1372,7 @@ jobs:
npm link ../sdk-internal
- name: Cache Native Module
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
id: cache
with:
path: |
@@ -1424,7 +1424,7 @@ jobs:
zip -r Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip Bitwarden.app
- name: Upload masdev artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip
path: apps/desktop/dist/mas-dev-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-masdev-universal.zip

View File

@@ -164,7 +164,7 @@ jobs:
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
- name: Upload ${{ matrix.name }} artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
@@ -274,7 +274,7 @@ jobs:
- name: Build Docker image
id: build-docker
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: apps/web
file: apps/web/Dockerfile
@@ -303,14 +303,14 @@ jobs:
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
uses: anchore/scan-action@869c549e657a088dc0441b08ce4fc0ecdac2bb65 # v5.3.0
with:
image: ${{ steps.image-name.outputs.name }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}

View File

@@ -43,7 +43,7 @@ jobs:
- name: Cache NPM
id: npm-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: "~/.npm"
key: ${{ runner.os }}-npm-chromatic-${{ hashFiles('**/package-lock.json') }}
@@ -56,7 +56,7 @@ jobs:
run: npm run build-storybook:ci
- name: Publish to Chromatic
uses: chromaui/action@dd2eecb9bef44f54774581f4163b0327fd8cf607 # v11.16.3
uses: chromaui/action@64a9c0ca3bfb724389b0d536e544f56b7b5ff5b3 # v11.20.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

View File

@@ -22,7 +22,7 @@ jobs:
crowdin_project_id: "308189"
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -158,42 +158,42 @@ jobs:
run: npm run dist:lin
- name: Upload .deb artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-amd64.deb
if-no-files-found: error
- name: Upload .rpm artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.rpm
if-no-files-found: error
- name: Upload .freebsd artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.freebsd
if-no-files-found: error
- name: Upload .snap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
path: apps/desktop/dist/bitwarden_${{ env._PACKAGE_VERSION }}_amd64.snap
if-no-files-found: error
- name: Upload .AppImage artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x86_64.AppImage
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}-linux.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-linux.yml
@@ -299,91 +299,91 @@ jobs:
-NewName bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
- name: Upload portable exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/Bitwarden-Portable-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload installer exe artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
path: apps/desktop/dist/nsis-web/Bitwarden-Installer-${{ env._PACKAGE_VERSION }}.exe
if-no-files-found: error
- name: Upload appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32.appx
if-no-files-found: error
- name: Upload store appx ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-ia32-store.appx
if-no-files-found: error
- name: Upload NSIS ia32 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-ia32.nsis.7z
if-no-files-found: error
- name: Upload appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64.appx
if-no-files-found: error
- name: Upload store appx x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-x64-store.appx
if-no-files-found: error
- name: Upload NSIS x64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-x64.nsis.7z
if-no-files-found: error
- name: Upload appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64.appx
if-no-files-found: error
- name: Upload store appx ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-arm64-store.appx
if-no-files-found: error
- name: Upload NSIS ARM64 artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
path: apps/desktop/dist/nsis-web/bitwarden-${{ env._PACKAGE_VERSION }}-arm64.nsis.7z
if-no-files-found: error
- name: Upload nupkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
path: apps/desktop/dist/chocolatey/bitwarden.${{ env._PACKAGE_VERSION }}.nupkg
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}.yml
path: apps/desktop/dist/nsis-web/${{ needs.setup.outputs.release-channel }}.yml
@@ -426,14 +426,14 @@ jobs:
- name: Cache Build
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Cache Safari
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -560,14 +560,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -707,28 +707,28 @@ jobs:
run: npm run pack:mac
- name: Upload .zip artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal-mac.zip
if-no-files-found: error
- name: Upload .dmg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg
if-no-files-found: error
- name: Upload .dmg blockmap artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
path: apps/desktop/dist/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.dmg.blockmap
if-no-files-found: error
- name: Upload auto-update artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ needs.setup.outputs.release-channel }}-mac.yml
path: apps/desktop/dist/${{ needs.setup.outputs.release-channel }}-mac.yml
@@ -773,14 +773,14 @@ jobs:
- name: Get Build Cache
id: build-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/desktop/build
key: ${{ runner.os }}-${{ github.run_id }}-build
- name: Setup Safari Cache
id: safari-cache
uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a # v4.1.2
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: apps/browser/dist/Safari
key: ${{ runner.os }}-${{ github.run_id }}-safari-extension
@@ -915,7 +915,7 @@ jobs:
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
- name: Upload .pkg artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg
path: apps/desktop/dist/mas-universal/Bitwarden-${{ env._PACKAGE_VERSION }}-universal.pkg

View File

@@ -66,7 +66,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@@ -115,7 +115,7 @@ jobs:
version: ${{ inputs.version_number_override }}
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@@ -452,7 +452,7 @@ jobs:
- setup
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -31,7 +31,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: cx_result.sarif

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@@ -2105,7 +2105,7 @@
"message": "Aikakatkaisutoiminnon vahvistus"
},
"autoFillAndSave": {
"message": "Täytä automaattisesti ja tallenna"
"message": "Automaattitäytä ja tallenna"
},
"fillAndSave": {
"message": "Täytä ja tallenna"
@@ -3482,7 +3482,7 @@
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Aika jäljellä, ennen kuin nykyinen TOTP vanhenee",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -4810,7 +4810,7 @@
"message": "Määritä kaksivaiheinen kirjautuminen"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden lähettää tilisi sähköpostiosoitteeseen koodin, jolla voit vahvistaa kirjautumiset uusista laitteista helmikuusta 2025 alkaen."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."

View File

@@ -1005,7 +1005,7 @@
"message": "Prikazuj identitete za jednostavnu auto-ispunu."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Klikni stavke za auto-ispunu na prikazu trezora"
},
"clearClipboard": {
"message": "Očisti međuspremnik",

View File

@@ -20,16 +20,16 @@
"message": "Crea account"
},
"newToBitwarden": {
"message": "New to Bitwarden?"
"message": "Nuovo in Bitwarden?"
},
"logInWithPasskey": {
"message": "Log in with passkey"
"message": "Accedi con passkey"
},
"useSingleSignOn": {
"message": "Use single sign-on"
"message": "Usa il Single Sign-On"
},
"welcomeBack": {
"message": "Welcome back"
"message": "Bentornato"
},
"setAStrongPassword": {
"message": "Imposta una password robusta"
@@ -120,7 +120,7 @@
"message": "Copia password"
},
"copyPassphrase": {
"message": "Copy passphrase"
"message": "Copia passphrase"
},
"copyNote": {
"message": "Copia nota"
@@ -153,13 +153,13 @@
"message": "Copia numero licenza"
},
"copyPrivateKey": {
"message": "Copy private key"
"message": "Copia chiave privata"
},
"copyPublicKey": {
"message": "Copy public key"
"message": "Copia chiave pubblica"
},
"copyFingerprint": {
"message": "Copy fingerprint"
"message": "Copia impronta"
},
"copyCustomField": {
"message": "Copia $FIELD$",
@@ -177,7 +177,7 @@
"message": "Copia note"
},
"fill": {
"message": "Fill",
"message": "Riempi",
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
@@ -193,10 +193,10 @@
"message": "Riempi automaticamente identità"
},
"fillVerificationCode": {
"message": "Fill verification code"
"message": "Riempi codice di verifica"
},
"fillVerificationCodeAria": {
"message": "Fill Verification Code",
"message": "Riempi Codice di Verifica",
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
},
"generatePasswordCopied": {
@@ -239,7 +239,7 @@
"message": "Aggiungi elemento"
},
"accountEmail": {
"message": "Account email"
"message": "Email dell'account"
},
"requestHint": {
"message": "Richiedi suggerimento"
@@ -443,7 +443,7 @@
"message": "Genera password"
},
"generatePassphrase": {
"message": "Generate passphrase"
"message": "Genera passphrase"
},
"regeneratePassword": {
"message": "Rigenera password"
@@ -530,7 +530,7 @@
"description": "Label for the avoid ambiguous characters checkbox."
},
"generatorPolicyInEffect": {
"message": "Enterprise policy requirements have been applied to your generator options.",
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni del generatore.",
"description": "Indicates that a policy limits the credential generator screen."
},
"searchVault": {
@@ -576,7 +576,7 @@
"message": "Note"
},
"privateNote": {
"message": "Private note"
"message": "Nota privata"
},
"note": {
"message": "Nota"
@@ -600,7 +600,7 @@
"message": "Avvia il sito web"
},
"launchWebsiteName": {
"message": "Launch website $ITEMNAME$",
"message": "Apri sito web $ITEMNAME$",
"placeholders": {
"itemname": {
"content": "$1",
@@ -633,7 +633,7 @@
"message": "Timeout della sessione"
},
"vaultTimeoutHeader": {
"message": "Vault timeout"
"message": "Timeout cassaforte"
},
"otherOptions": {
"message": "Altre opzioni"
@@ -651,13 +651,13 @@
"message": "La tua cassaforte è bloccata. Verifica la tua identità per continuare."
},
"yourVaultIsLockedV2": {
"message": "Your vault is locked"
"message": "Cassaforte bloccata"
},
"yourAccountIsLocked": {
"message": "Your account is locked"
"message": "Il tuo account è bloccato"
},
"or": {
"message": "or"
"message": "o"
},
"unlock": {
"message": "Sblocca"
@@ -852,7 +852,7 @@
"message": "Accedi"
},
"logInToBitwarden": {
"message": "Log in to Bitwarden"
"message": "Accedi a Bitwarden"
},
"restartRegistration": {
"message": "Riprova la registrazione"
@@ -888,10 +888,10 @@
"message": "La verifica in due passaggi rende il tuo account più sicuro richiedendoti di verificare il tuo login usando un altro dispositivo come una chiave di sicurezza, app di autenticazione, SMS, telefonata, o email. Può essere abilitata nella cassaforte web su bitwarden.com. Vuoi visitare il sito?"
},
"twoStepLoginConfirmationContent": {
"message": "Make your account more secure by setting up two-step login in the Bitwarden web app."
"message": "Rendi il tuo account più sicuro impostando l'autenticazione a due fattori nell'app web di Bitwarden."
},
"twoStepLoginConfirmationTitle": {
"message": "Continue to web app?"
"message": "Aprire web app?"
},
"editedFolder": {
"message": "Cartella salvata"
@@ -1005,7 +1005,7 @@
"message": "Mostra le identità nella sezione Scheda per riempirle automaticamente."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Clicca gli oggetti da riempire dalla sezione Cassaforte"
},
"clearClipboard": {
"message": "Cancella appunti",
@@ -1126,7 +1126,7 @@
"description": "WARNING (should stay in capitalized letters if the language permits)"
},
"warningCapitalized": {
"message": "Warning",
"message": "Attenzione",
"description": "Warning (should maintain locale-relevant capitalization)"
},
"confirmVaultExport": {
@@ -1206,7 +1206,7 @@
"message": "File"
},
"fileToShare": {
"message": "File to share"
"message": "File da condividere"
},
"selectFile": {
"message": "Seleziona un file"
@@ -1317,10 +1317,10 @@
"message": "Inserisci il codice di verifica a 6 cifre dalla tua app di autenticazione."
},
"authenticationTimeout": {
"message": "Authentication timeout"
"message": "Timeout autenticazione"
},
"authenticationSessionTimedOut": {
"message": "The authentication session timed out. Please restart the login process."
"message": "La sessione di autenticazione è scaduta. Accedi di nuovo."
},
"enterVerificationCodeEmail": {
"message": "Inserisci il codice di verifica a 6 cifre inviato a $EMAIL$.",
@@ -1440,7 +1440,7 @@
"message": "URL del server"
},
"selfHostBaseUrl": {
"message": "Self-host server URL",
"message": "URL server autogestito",
"description": "Label for field requesting a self-hosted integration service URL"
},
"apiUrl": {
@@ -1472,10 +1472,10 @@
"message": "Mostra suggerimenti di riempimento automatico nei campi del modulo"
},
"showInlineMenuIdentitiesLabel": {
"message": "Display identities as suggestions"
"message": "Mostra identità come consigli"
},
"showInlineMenuCardsLabel": {
"message": "Display cards as suggestions"
"message": "Mostra carte come consigli"
},
"showInlineMenuOnIconSelectionLabel": {
"message": "Mostra suggerimenti quando l'icona è selezionata"
@@ -1768,7 +1768,7 @@
"message": "Identità"
},
"typeSshKey": {
"message": "SSH key"
"message": "Chiave SSH"
},
"newItemHeader": {
"message": "Nuovo $TYPE$",
@@ -1801,13 +1801,13 @@
"message": "Cronologia delle password"
},
"generatorHistory": {
"message": "Generator history"
"message": "Cronologia generatore"
},
"clearGeneratorHistoryTitle": {
"message": "Clear generator history"
"message": "Cancella cronologia generatore"
},
"cleargGeneratorHistoryDescription": {
"message": "If you continue, all entries will be permanently deleted from generator's history. Are you sure you want to continue?"
"message": "Se continui, tutte le voci verranno eliminate definitivamente dalla cronologia del generatore. Vuoi continuare?"
},
"back": {
"message": "Indietro"
@@ -1846,7 +1846,7 @@
"message": "Note sicure"
},
"sshKeys": {
"message": "SSH Keys"
"message": "Chiavi SSH"
},
"clear": {
"message": "Cancella",
@@ -1929,10 +1929,10 @@
"message": "Cancella cronologia"
},
"nothingToShow": {
"message": "Nothing to show"
"message": "Niente da mostrare"
},
"nothingGeneratedRecently": {
"message": "You haven't generated anything recently"
"message": "Non hai generato niente di recente"
},
"remove": {
"message": "Rimuovi"
@@ -1993,16 +1993,16 @@
"message": "Sblocca con PIN"
},
"setYourPinTitle": {
"message": "Set PIN"
"message": "Imposta PIN"
},
"setYourPinButton": {
"message": "Set PIN"
"message": "Imposta PIN"
},
"setYourPinCode": {
"message": "Imposta il tuo codice PIN per sbloccare Bitwarden. Le tue impostazioni PIN saranno resettate se esci completamente dall'app."
},
"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": "Il tuo PIN sarà usato per sbloccare Bitwarden invece della password principale. Il PIN sarà ripristinato se ti disconnetterai completamente da Bitwarden."
},
"pinRequired": {
"message": "Codice PIN obbligatorio."
@@ -2017,7 +2017,7 @@
"message": "Sblocca con i dati biometrici"
},
"unlockWithMasterPassword": {
"message": "Unlock with master password"
"message": "Sblocca con password principale"
},
"awaitDesktop": {
"message": "In attesa di conferma dal desktop"
@@ -2029,7 +2029,7 @@
"message": "Blocca con la password principale al riavvio del browser"
},
"lockWithMasterPassOnRestart1": {
"message": "Require master password on browser restart"
"message": "Richiedi password principale al riavvio del browser"
},
"selectOneCollection": {
"message": "Devi selezionare almeno una raccolta."
@@ -2067,7 +2067,7 @@
"message": "Azione timeout cassaforte"
},
"vaultTimeoutAction1": {
"message": "Timeout action"
"message": "Azione al timeout"
},
"lock": {
"message": "Blocca",
@@ -2355,14 +2355,14 @@
"message": "Modifiche del dominio escluso salvate"
},
"limitSendViews": {
"message": "Limit views"
"message": "Limita visualizzazioni"
},
"limitSendViewsHint": {
"message": "No one can view this Send after the limit is reached.",
"message": "Nessuno potrà vedere questo Send al raggiungimento del limite.",
"description": "Displayed under the limit views field on Send"
},
"limitSendViewsCount": {
"message": "$ACCESSCOUNT$ views left",
"message": "$ACCESSCOUNT$ visualizzazioni rimaste",
"description": "Displayed under the limit views field on Send",
"placeholders": {
"accessCount": {
@@ -2376,14 +2376,14 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendDetails": {
"message": "Send details",
"message": "Dettagli Send",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendTypeText": {
"message": "Testo"
},
"sendTypeTextToShare": {
"message": "Text to share"
"message": "Testo da condividere"
},
"sendTypeFile": {
"message": "File"
@@ -2393,7 +2393,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": "Nascondi testo come default"
},
"expired": {
"message": "Scaduto"
@@ -2440,7 +2440,7 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"deleteSendPermanentConfirmation": {
"message": "Are you sure you want to permanently delete this Send?",
"message": "Sicuro di voler eliminare definitivamente questo Send?",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"editSend": {
@@ -2451,7 +2451,7 @@
"message": "Data di eliminazione"
},
"deletionDateDescV2": {
"message": "The Send will be permanently deleted on this date.",
"message": "Il Send sarà cancellato definitivamente in questa data.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"expirationDate": {
@@ -2473,7 +2473,7 @@
"message": "Personalizzato"
},
"sendPasswordDescV3": {
"message": "Add an optional password for recipients to access this Send.",
"message": "Richiedi ai destinatari una password opzionale per aprire questo Send.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"createSend": {
@@ -2500,11 +2500,11 @@
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInHoursSingle": {
"message": "The Send will be available to anyone with the link for the next 1 hour.",
"message": "Il Send sarà disponibile a chiunque con il link per la prossima ora.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInHours": {
"message": "The Send will be available to anyone with the link for the next $HOURS$ hours.",
"message": "Il Send sarà disponibile a chiunque con il link per le prossime $HOURS$ ore.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
"hours": {
@@ -2514,11 +2514,11 @@
}
},
"sendExpiresInDaysSingle": {
"message": "The Send will be available to anyone with the link for the next 1 day.",
"message": "Il Send sarà disponibile a chiunque con il link per il prossimo giorno.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendExpiresInDays": {
"message": "The Send will be available to anyone with the link for the next $DAYS$ days.",
"message": "Il Send sarà disponibile a chiunque con il link per i prossimi $DAYS$ giorni.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated.",
"placeholders": {
"days": {
@@ -2536,11 +2536,11 @@
"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": "Scollegare estensione?",
"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": "Per creare un file Send, devi scollegare l'estensione in una nuova finestra.",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendLinuxChromiumFileWarning": {
@@ -2553,7 +2553,7 @@
"message": "Per scegliere un file usando Safari, apri una nuova finestra cliccando questo banner."
},
"popOut": {
"message": "Pop out"
"message": "Scollega"
},
"sendFileCalloutHeader": {
"message": "Prima di iniziare"
@@ -2574,7 +2574,7 @@
"message": "Si è verificato un errore durante il salvataggio delle date di eliminazione e scadenza."
},
"hideYourEmail": {
"message": "Hide your email address from viewers."
"message": "Nascondi il tuo indirizzo email ai visualizzatori."
},
"passwordPrompt": {
"message": "Richiedi di inserire la password principale di nuovo per visualizzare questo elemento"
@@ -2631,7 +2631,7 @@
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
"message": "out of $TOTAL$",
"message": "di $TOTAL$",
"placeholders": {
"total": {
"content": "$1",
@@ -2650,7 +2650,7 @@
"message": "Minuti"
},
"vaultTimeoutPolicyAffectingOptions": {
"message": "Enterprise policy requirements have been applied to your timeout options"
"message": "I requisiti di politica aziendale sono stati applicati alle opzioni di timeout"
},
"vaultTimeoutPolicyInEffect": {
"message": "Le politiche della tua organizzazione hanno impostato il timeout massimo consentito della tua cassaforte su $HOURS$ ore e $MINUTES$ minuti.",
@@ -2666,7 +2666,7 @@
}
},
"vaultTimeoutPolicyInEffect1": {
"message": "$HOURS$ hour(s) and $MINUTES$ minute(s) maximum.",
"message": "Al massimo $HOURS$ ora/e e $MINUTES$ minuto/i.",
"placeholders": {
"hours": {
"content": "$1",
@@ -2679,7 +2679,7 @@
}
},
"vaultTimeoutPolicyMaximumError": {
"message": "Timeout exceeds the restriction set by your organization: $HOURS$ hour(s) and $MINUTES$ minute(s) maximum",
"message": "Il timeout supera la restrizione impostata dalla tua organizzazione: massimo $HOURS$ ora/e e $MINUTES$ minuto/i",
"placeholders": {
"hours": {
"content": "$1",
@@ -2793,10 +2793,10 @@
"message": "Genera nome utente"
},
"generateEmail": {
"message": "Generate email"
"message": "Genera email"
},
"spinboxBoundariesHint": {
"message": "Value must be between $MIN$ and $MAX$.",
"message": "Il valore deve essere compreso tra $MIN$ e $MAX$.",
"description": "Explains spin box minimum and maximum values to the user",
"placeholders": {
"min": {
@@ -2810,7 +2810,7 @@
}
},
"passwordLengthRecommendationHint": {
"message": " Use $RECOMMENDED$ characters or more to generate a strong password.",
"message": " Usa $RECOMMENDED$ caratteri o più per generare una password forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a length to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2820,7 +2820,7 @@
}
},
"passphraseNumWordsRecommendationHint": {
"message": " Use $RECOMMENDED$ words or more to generate a strong passphrase.",
"message": " Usa $RECOMMENDED$ parole o più per generare una passphrase forte.",
"description": "Appended to `spinboxBoundariesHint` to recommend a number of words to the user. This must include any language-specific 'sentence' separator characters (e.g. a space in english).",
"placeholders": {
"recommended": {
@@ -2861,11 +2861,11 @@
"message": "Genera un alias email con un servizio di inoltro esterno."
},
"forwarderDomainName": {
"message": "Email domain",
"message": "Dominio email",
"description": "Labels the domain name email forwarder service option"
},
"forwarderDomainNameHint": {
"message": "Choose a domain that is supported by the selected service",
"message": "Scegli un dominio supportato dal servizio selezionato",
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
@@ -3068,25 +3068,25 @@
"message": "Invia notifica di nuovo"
},
"viewAllLogInOptions": {
"message": "View all log in options"
"message": "Visualizza tutte le opzioni di accesso"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Visualizza tutte le opzioni di accesso"
},
"notificationSentDevice": {
"message": "Una notifica è stata inviata al tuo dispositivo."
},
"aNotificationWasSentToYourDevice": {
"message": "A notification was sent to your device"
"message": "Una notifica è stata inviata al tuo dispositivo"
},
"makeSureYourAccountIsUnlockedAndTheFingerprintEtc": {
"message": "Make sure your account is unlocked and the fingerprint phrase matches on the other device"
"message": "Assicurati che il tuo account sia sbloccato e che la frase dell'impronta digitale corrisponda nell'altro dispositivo"
},
"youWillBeNotifiedOnceTheRequestIsApproved": {
"message": "You will be notified once the request is approved"
"message": "Sarai notificato una volta che la richiesta sarà approvata"
},
"needAnotherOptionV1": {
"message": "Need another option?"
"message": "Bisogno di un'altra opzione?"
},
"loginInitiated": {
"message": "Accesso avviato"
@@ -3182,16 +3182,16 @@
"message": "Si apre in una nuova finestra"
},
"rememberThisDeviceToMakeFutureLoginsSeamless": {
"message": "Remember this device to make future logins seamless"
"message": "Ricorda questo dispositivo per rendere immediati i futuri accessi"
},
"deviceApprovalRequired": {
"message": "Approvazione del dispositivo obbligatoria. Seleziona un'opzione di approvazione:"
},
"deviceApprovalRequiredV2": {
"message": "Device approval required"
"message": "Approvazione dispositivo richiesta"
},
"selectAnApprovalOptionBelow": {
"message": "Select an approval option below"
"message": "Seleziona un'opzione di approvazione sotto"
},
"rememberThisDevice": {
"message": "Ricorda questo dispositivo"
@@ -3267,7 +3267,7 @@
"message": "Email utente mancante"
},
"activeUserEmailNotFoundLoggingYouOut": {
"message": "Active user email not found. Logging you out."
"message": "Email utente attiva non trovata. Logout in corso."
},
"deviceTrusted": {
"message": "Dispositivo fidato"
@@ -3478,11 +3478,11 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"totpCodeAria": {
"message": "Time-based One-Time Password Verification Code",
"message": "Codice di Verifica One-Time a tempo",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Tempo rimasto prima che l'attuale TOTP scada",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -3711,10 +3711,10 @@
"message": "Passkey"
},
"accessing": {
"message": "Accessing"
"message": "Accedendo a"
},
"loggedInExclamation": {
"message": "Logged in!"
"message": "Accesso effettuato!"
},
"passkeyNotCopied": {
"message": "La passkey non sarà copiata"
@@ -3741,7 +3741,7 @@
"message": "Nessun login corrispondente per questo sito"
},
"searchSavePasskeyNewLogin": {
"message": "Search or save passkey as new login"
"message": "Cerca o salva la passkey come nuovo login"
},
"confirm": {
"message": "Conferma"
@@ -4208,13 +4208,13 @@
"message": "Filtri"
},
"filterVault": {
"message": "Filter vault"
"message": "Filtra cassaforte"
},
"filterApplied": {
"message": "One filter applied"
"message": "Un filtro applicato"
},
"filterAppliedPlural": {
"message": "$COUNT$ filters applied",
"message": "$COUNT$ filtri applicati",
"placeholders": {
"count": {
"content": "$1",
@@ -4328,7 +4328,7 @@
"message": "Abilita animazioni"
},
"showAnimations": {
"message": "Show animations"
"message": "Mostra animazioni"
},
"addAccount": {
"message": "Aggiungi account"
@@ -4546,13 +4546,13 @@
"message": "Posizione elemento"
},
"fileSend": {
"message": "File Send"
"message": "Send di File"
},
"fileSends": {
"message": "Send File"
},
"textSend": {
"message": "Text Send"
"message": "Send di Testo"
},
"textSends": {
"message": "Send Testo"
@@ -4570,7 +4570,7 @@
"message": "Mostra il numero di suggerimenti di riempimento automatico sull'icona dell'estensione"
},
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
"message": "Mostra azioni di copia rapida nella Cassaforte"
},
"systemDefault": {
"message": "Predefinito del sistema"
@@ -4579,37 +4579,37 @@
"message": "I requisiti della policy aziendale sono stati applicati a questa impostazione"
},
"sshPrivateKey": {
"message": "Private key"
"message": "Chiave privata"
},
"sshPublicKey": {
"message": "Public key"
"message": "Chiave pubblica"
},
"sshFingerprint": {
"message": "Fingerprint"
"message": "Impronta digitale"
},
"sshKeyAlgorithm": {
"message": "Key type"
"message": "Tipo di chiave"
},
"sshKeyAlgorithmED25519": {
"message": "ED25519"
},
"sshKeyAlgorithmRSA2048": {
"message": "RSA 2048-Bit"
"message": "RSA a 2048 bit"
},
"sshKeyAlgorithmRSA3072": {
"message": "RSA 3072-Bit"
"message": "RSA a 3072 bit"
},
"sshKeyAlgorithmRSA4096": {
"message": "RSA 4096-Bit"
"message": "RSA a 4096 bit"
},
"retry": {
"message": "Retry"
"message": "Riprova"
},
"vaultCustomTimeoutMinimum": {
"message": "Minimum custom timeout is 1 minute."
"message": "Il timeout personalizzato minimo è 1 minuto."
},
"additionalContentAvailable": {
"message": "Additional content is available"
"message": "Sono disponibili ulteriori contenuti"
},
"fileSavedToDevice": {
"message": "File salvato sul dispositivo. Gestisci dai download del dispositivo."
@@ -4642,22 +4642,22 @@
"message": "Non hai i permessi per modificare questo elemento"
},
"authenticating": {
"message": "Authenticating"
"message": "Autenticazione"
},
"fillGeneratedPassword": {
"message": "Fill generated password",
"message": "Riempi password generata",
"description": "Heading for the password generator within the inline menu"
},
"passwordRegenerated": {
"message": "Password regenerated",
"message": "Password rigenerata",
"description": "Notification message for when a password has been regenerated"
},
"saveLoginToBitwarden": {
"message": "Save login to Bitwarden?",
"message": "Salvare il login su Bitwarden?",
"description": "Confirmation message for saving a login to Bitwarden"
},
"spaceCharacterDescriptor": {
"message": "Space",
"message": "Spazio",
"description": "Represents the space key in screen reader content as a readable word"
},
"tildeCharacterDescriptor": {
@@ -4669,157 +4669,157 @@
"description": "Represents the ` key in screen reader content as a readable word"
},
"exclamationCharacterDescriptor": {
"message": "Exclamation mark",
"message": "Punto esclamativo",
"description": "Represents the ! key in screen reader content as a readable word"
},
"atSignCharacterDescriptor": {
"message": "At sign",
"message": "Chiocciola",
"description": "Represents the @ key in screen reader content as a readable word"
},
"hashSignCharacterDescriptor": {
"message": "Hash sign",
"message": "Cancelletto",
"description": "Represents the # key in screen reader content as a readable word"
},
"dollarSignCharacterDescriptor": {
"message": "Dollar sign",
"message": "Simbolo del dollaro",
"description": "Represents the $ key in screen reader content as a readable word"
},
"percentSignCharacterDescriptor": {
"message": "Percent sign",
"message": "Segno di percentuale",
"description": "Represents the % key in screen reader content as a readable word"
},
"caretCharacterDescriptor": {
"message": "Caret",
"message": "Accento circonflesso",
"description": "Represents the ^ key in screen reader content as a readable word"
},
"ampersandCharacterDescriptor": {
"message": "Ampersand",
"message": "E commerciale",
"description": "Represents the & key in screen reader content as a readable word"
},
"asteriskCharacterDescriptor": {
"message": "Asterisk",
"message": "Asterisco",
"description": "Represents the * key in screen reader content as a readable word"
},
"parenLeftCharacterDescriptor": {
"message": "Left parenthesis",
"message": "Parentesi sinistra",
"description": "Represents the ( key in screen reader content as a readable word"
},
"parenRightCharacterDescriptor": {
"message": "Right parenthesis",
"message": "Parentesi destra",
"description": "Represents the ) key in screen reader content as a readable word"
},
"hyphenCharacterDescriptor": {
"message": "Underscore",
"message": "Trattino basso",
"description": "Represents the _ key in screen reader content as a readable word"
},
"underscoreCharacterDescriptor": {
"message": "Hyphen",
"message": "Trattino",
"description": "Represents the - key in screen reader content as a readable word"
},
"plusCharacterDescriptor": {
"message": "Plus",
"message": "P",
"description": "Represents the + key in screen reader content as a readable word"
},
"equalsCharacterDescriptor": {
"message": "Equals",
"message": "Uguale",
"description": "Represents the = key in screen reader content as a readable word"
},
"braceLeftCharacterDescriptor": {
"message": "Left brace",
"message": "Parentesi graffa aperta",
"description": "Represents the { key in screen reader content as a readable word"
},
"braceRightCharacterDescriptor": {
"message": "Right brace",
"message": "Parentesi graffa chiusa",
"description": "Represents the } key in screen reader content as a readable word"
},
"bracketLeftCharacterDescriptor": {
"message": "Left bracket",
"message": "Parentesi quadra aperta",
"description": "Represents the [ key in screen reader content as a readable word"
},
"bracketRightCharacterDescriptor": {
"message": "Right bracket",
"message": "Parentesi quadra chiusa",
"description": "Represents the ] key in screen reader content as a readable word"
},
"pipeCharacterDescriptor": {
"message": "Pipe",
"message": "Barra verticale",
"description": "Represents the | key in screen reader content as a readable word"
},
"backSlashCharacterDescriptor": {
"message": "Back slash",
"message": "Barra rovesciata",
"description": "Represents the back slash key in screen reader content as a readable word"
},
"colonCharacterDescriptor": {
"message": "Colon",
"message": "Due punti",
"description": "Represents the : key in screen reader content as a readable word"
},
"semicolonCharacterDescriptor": {
"message": "Semicolon",
"message": "Punto e virgola",
"description": "Represents the ; key in screen reader content as a readable word"
},
"doubleQuoteCharacterDescriptor": {
"message": "Double quote",
"message": "Doppi apici",
"description": "Represents the double quote key in screen reader content as a readable word"
},
"singleQuoteCharacterDescriptor": {
"message": "Single quote",
"message": "Apostrofo",
"description": "Represents the ' key in screen reader content as a readable word"
},
"lessThanCharacterDescriptor": {
"message": "Less than",
"message": "Minore",
"description": "Represents the < key in screen reader content as a readable word"
},
"greaterThanCharacterDescriptor": {
"message": "Greater than",
"message": "Maggiore",
"description": "Represents the > key in screen reader content as a readable word"
},
"commaCharacterDescriptor": {
"message": "Comma",
"message": "Virgola",
"description": "Represents the , key in screen reader content as a readable word"
},
"periodCharacterDescriptor": {
"message": "Period",
"message": "Punto",
"description": "Represents the . key in screen reader content as a readable word"
},
"questionCharacterDescriptor": {
"message": "Question mark",
"message": "Punto interrogativo",
"description": "Represents the ? key in screen reader content as a readable word"
},
"forwardSlashCharacterDescriptor": {
"message": "Forward slash",
"message": "Slash",
"description": "Represents the / key in screen reader content as a readable word"
},
"lowercaseAriaLabel": {
"message": "Lowercase"
"message": "Minuscolo"
},
"uppercaseAriaLabel": {
"message": "Uppercase"
"message": "Maiuscolo"
},
"generatedPassword": {
"message": "Generated password"
"message": "Password generata"
},
"compactMode": {
"message": "Compact mode"
"message": "Modalità compatta"
},
"beta": {
"message": "Beta"
},
"importantNotice": {
"message": "Important notice"
"message": "Avviso importante"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "Imposta accesso in due passaggi"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden invierà un codice all'email del tuo account per verificare gli accessi da nuovi dispositivi a partire da febbraio 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "Puoi impostare l'accesso in due passaggi come modo alternativo per proteggere il tuo account, o cambiare la tua e-mail in una alla quale puoi accedere."
},
"remindMeLater": {
"message": "Remind me later"
"message": "Ricordamelo più tardi"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "Riesci ancora ad accedere a questa email, $EMAIL$?",
"placeholders": {
"email": {
"content": "$1",
@@ -4828,24 +4828,24 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "No, non riesco"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "Sì, riesco ad accedere a questa email"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "Attiva accesso in due passaggi"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "Cambia l'email dell'account"
},
"extensionWidth": {
"message": "Extension width"
"message": "Larghezza estensione"
},
"wide": {
"message": "Wide"
"message": "Larga"
},
"extraWide": {
"message": "Extra wide"
"message": "Molto larga"
}
}

View File

@@ -1005,7 +1005,7 @@
"message": "自動入力を簡単にするために、タブページに ID アイテムを表示します"
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "保管庫で、自動入力するアイテムをクリックしてください"
},
"clearClipboard": {
"message": "クリップボードの消去",
@@ -4810,10 +4810,10 @@
"message": "2段階認証を設定する"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden は2025年2月以降、新しいデバイスからのログイン時にアカウントのメールアドレスに確認コードを送信します。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "代わりに2段階認証によるログインでアカウントを保護するか、メールアドレスをあなたがアクセスできるものに変更できます。"
},
"remindMeLater": {
"message": "後で再通知"
@@ -4831,13 +4831,13 @@
"message": "いいえ、違います。"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "はい、メールアドレスには私が確実にアクセスできます"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "2段階認証によるログインを有効にする"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "アカウントのメールアドレスを変更する"
},
"extensionWidth": {
"message": "拡張機能の幅"

View File

@@ -181,7 +181,7 @@
"description": "This string is used on the vault page to indicate autofilling. Horizontal space is limited in the interface here so try and keep translations as concise as possible."
},
"autoFill": {
"message": "Auto-invullen"
"message": "Automatisch invullen"
},
"autoFillLogin": {
"message": "Login automatisch invullen"
@@ -688,7 +688,7 @@
"message": "Nu vergrendelen"
},
"lockAll": {
"message": "Vergrendel alles"
"message": "Alles vergrendelen"
},
"immediately": {
"message": "Onmiddellijk"
@@ -1043,7 +1043,7 @@
"message": "Ja, nu bijwerken"
},
"notificationUnlockDesc": {
"message": "Ontgrendel je Bitwarden-kluis om het auto-invulverzoek te voltooien."
"message": "Ontgrendel je Bitwarden-kluis om het automatisch invullen verzoek te voltooien."
},
"notificationUnlock": {
"message": "Ontgrendelen"
@@ -1200,7 +1200,7 @@
"message": "Geen bijlagen."
},
"attachmentSaved": {
"message": "De bijlage is opgeslagen."
"message": "Bijlage opgeslagen"
},
"file": {
"message": "Bestand"
@@ -1209,7 +1209,7 @@
"message": "Bestand om te delen"
},
"selectFile": {
"message": "Selecteer een bestand."
"message": "Selecteer een bestand"
},
"maxFileSize": {
"message": "Maximale bestandsgrootte is 500 MB."
@@ -1242,7 +1242,7 @@
"message": "1 GB versleutelde opslag voor bijlagen."
},
"premiumSignUpEmergency": {
"message": "Noodtoegang"
"message": "Noodtoegang."
},
"premiumSignUpTwoStepOptions": {
"message": "Eigen opties voor tweestapsaanmelding zoals YubiKey en Duo."
@@ -1425,10 +1425,10 @@
"message": "Specificeer de basis-URL van je zelfgehoste Bitwarden-installatie. Bijvoorbeeld: https://bitwarden.company.com"
},
"selfHostedCustomEnvHeader": {
"message": "For advanced configuration, you can specify the base URL of each service independently."
"message": "Voor geavanceerde configuratie kun je de basis URL van elke service onafhankelijk opgeven."
},
"selfHostedEnvFormInvalid": {
"message": "You must add either the base Server URL or at least one custom environment."
"message": "Je moet de basis URL van de server of ten minste één aangepaste omgeving toevoegen."
},
"customEnvironment": {
"message": "Aangepaste omgeving"
@@ -1459,14 +1459,14 @@
"message": "Pictogrammenserver-URL"
},
"environmentSaved": {
"message": "De omgeving-URL's zijn opgeslagen."
"message": "Omgeving-URL's opgeslagen"
},
"showAutoFillMenuOnFormFields": {
"message": "Auto-invulmenu op formuliervelden weergeven",
"message": "Automatisch invullen menu op formuliervelden weergeven",
"description": "Represents the message for allowing the user to enable the autofill overlay"
},
"autofillSuggestionsSectionTitle": {
"message": "Suggesties voor automatisch invullen"
"message": "Suggesties automatisch invullen"
},
"showInlineMenuLabel": {
"message": "Suggesties voor automatisch invullen op formuliervelden weergeven"
@@ -1511,7 +1511,7 @@
"message": "Als een inlogformulier wordt gedetecteerd, dan worden de inloggegevens automatisch ingevuld."
},
"experimentalFeature": {
"message": "Gehackte of onbetrouwbare websites kunnen auto-invullen tijdens het laden van de pagina misbruiken."
"message": "Gehackte of onbetrouwbare websites kunnen automatisch invullen tijdens het laden van de pagina misbruiken."
},
"learnMoreAboutAutofillOnPageLoadLinkText": {
"message": "Meer over risico's lezen"
@@ -1535,7 +1535,7 @@
"message": "Automatisch invullen bij laden van pagina"
},
"autoFillOnPageLoadNo": {
"message": "Niet Automatisch invullen bij laden van pagina"
"message": "Niet automatisch invullen bij laden van pagina"
},
"commandOpenPopup": {
"message": "Open kluis in pop-up"
@@ -1606,7 +1606,7 @@
"message": "Een herkenbare afbeelding naast iedere login weergeven."
},
"faviconDescAlt": {
"message": "Show a recognizable image next to each login. Applies to all logged in accounts."
"message": "Toon een herkenbare afbeelding naast elke login. Geldt voor alle ingelogde accounts."
},
"enableBadgeCounter": {
"message": "Teller weergeven"
@@ -1690,7 +1690,7 @@
"message": "Dr."
},
"mx": {
"message": "Mx"
"message": "Mx."
},
"firstName": {
"message": "Voornaam"
@@ -1993,10 +1993,10 @@
"message": "Ontgrendelen met PIN"
},
"setYourPinTitle": {
"message": "PIN-code instellen"
"message": "PIN instellen"
},
"setYourPinButton": {
"message": "PIN-code instellen"
"message": "PIN instellen"
},
"setYourPinCode": {
"message": "Stel je PIN-code in voor het ontgrendelen van Bitwarden. Je PIN-code wordt opnieuw ingesteld als je je ooit volledig afmeldt bij de applicatie."
@@ -2111,10 +2111,10 @@
"message": "Invullen en opslaan"
},
"autoFillSuccessAndSavedUri": {
"message": "Automatisch gevuld item en opgeslagen URI"
"message": "Automatisch ingevuld item en opgeslagen URI"
},
"autoFillSuccess": {
"message": "Automatisch gevuld item"
"message": "Item automatisch ingevuld "
},
"insecurePageWarning": {
"message": "Waarschuwing: Dit is een onbeveiligde HTTP-pagina waardoor anderen alle informatie die je verstuurt kunnen zien en wijzigen. Deze login is oorspronkelijk opgeslagen op een beveiligde (HTTPS) pagina."
@@ -2225,7 +2225,7 @@
"message": "Fout bij vernieuwen toegangstoken"
},
"errorRefreshingAccessTokenDesc": {
"message": "No refresh token or API keys found. Please try logging out and logging back in."
"message": "Geen verversingstoken of API-sleutels gevonden. Probeer uit te loggen en weer in te loggen."
},
"desktopSyncVerificationTitle": {
"message": "Desktopsynchronisatieverificatie"
@@ -2282,10 +2282,10 @@
"message": "Dit apparaat ondersteunt geen browserbiometrie."
},
"biometricsNotUnlockedTitle": {
"message": "User locked or logged out"
"message": "Gebruiker vergrendeld of uitgelogd"
},
"biometricsNotUnlockedDesc": {
"message": "Please unlock this user in the desktop application and try again."
"message": "Ontgrendel deze gebruiker in de desktopapplicatie en probeer het opnieuw."
},
"biometricsNotAvailableTitle": {
"message": "Biometrisch ontgrendelen niet beschikbaar"
@@ -2623,11 +2623,11 @@
"description": "Used as a message within the notification bar when no folders are found"
},
"orgPermissionsUpdatedMustSetPassword": {
"message": "Your organization permissions were updated, requiring you to set a master password.",
"message": "De machtigingen van je organisatie zijn bijgewerkt, waardoor je een hoofdwachtwoord moet instellen.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"orgRequiresYouToSetPassword": {
"message": "Your organization requires you to set a master password.",
"message": "Je organisatie vereist dat je een hoofdwachtwoord instelt.",
"description": "Used as a card title description on the set password page to explain why the user is there"
},
"cardMetrics": {
@@ -2766,7 +2766,7 @@
"message": "Persoonlijke kluis exporteren"
},
"exportingIndividualVaultDescription": {
"message": "Only the individual vault items associated with $EMAIL$ will be exported. Organization vault items will not be included. Only vault item information will be exported and will not include associated attachments.",
"message": "Alleen de individuele kluisitems die gekoppeld zijn aan $EMAIL$ worden geëxporteerd. Kluisitems van organisaties worden niet meegenomen. Alleen de informatie over het kluisitem wordt geëxporteerd en niet de bijbehorende bijlagen.",
"placeholders": {
"email": {
"content": "$1",
@@ -2869,7 +2869,7 @@
"description": "Guidance provided for email forwarding services that support multiple email domains."
},
"forwarderError": {
"message": "$SERVICENAME$ error: $ERRORMESSAGE$",
"message": "$SERVICENAME$ fout: $ERRORMESSAGE$",
"description": "Reports an error returned by a forwarding service to the user.",
"placeholders": {
"servicename": {
@@ -2887,7 +2887,7 @@
"description": "Displayed with the address on the forwarding service's configuration screen."
},
"forwarderGeneratedByWithWebsite": {
"message": "Website: $WEBSITE$. Generated by Bitwarden.",
"message": "Website: $WEBSITE$. Gegenereerd door Bitwarden.",
"description": "Displayed with the address on the forwarding service's configuration screen.",
"placeholders": {
"WEBSITE": {
@@ -2897,7 +2897,7 @@
}
},
"forwaderInvalidToken": {
"message": "Invalid $SERVICENAME$ API token",
"message": "Ongeldig $SERVICENAME$ API token",
"description": "Displayed when the user's API token is empty or rejected by the forwarding service.",
"placeholders": {
"servicename": {
@@ -2907,7 +2907,7 @@
}
},
"forwaderInvalidTokenWithMessage": {
"message": "Invalid $SERVICENAME$ API token: $ERRORMESSAGE$",
"message": "Ongeldige $SERVICENAME$ API token: $ERRORMESSAGE$",
"description": "Displayed when the user's API token is rejected by the forwarding service with an error message.",
"placeholders": {
"servicename": {
@@ -2921,7 +2921,7 @@
}
},
"forwarderNoAccountId": {
"message": "Unable to obtain $SERVICENAME$ masked email account ID.",
"message": "Kan $SERVICENAME$ gemaskeerde e-mailaccount-ID niet verkrijgen.",
"description": "Displayed when the forwarding service fails to return an account ID.",
"placeholders": {
"servicename": {
@@ -2931,7 +2931,7 @@
}
},
"forwarderNoDomain": {
"message": "Invalid $SERVICENAME$ domain.",
"message": "Ongeldig $SERVICENAME$ domein.",
"description": "Displayed when the domain is empty or domain authorization failed at the forwarding service.",
"placeholders": {
"servicename": {
@@ -2941,7 +2941,7 @@
}
},
"forwarderNoUrl": {
"message": "Invalid $SERVICENAME$ url.",
"message": "Ongeldige $SERVICENAME$ url.",
"description": "Displayed when the url of the forwarding service wasn't supplied.",
"placeholders": {
"servicename": {
@@ -2951,7 +2951,7 @@
}
},
"forwarderUnknownError": {
"message": "Unknown $SERVICENAME$ error occurred.",
"message": "Onbekende $SERVICENAME$ fout opgetreden.",
"description": "Displayed when the forwarding service failed due to an unknown error.",
"placeholders": {
"servicename": {
@@ -2961,7 +2961,7 @@
}
},
"forwarderUnknownForwarder": {
"message": "Unknown forwarder: '$SERVICENAME$'.",
"message": "Onbekende doorstuurder: '$SERVICENAME$'.",
"description": "Displayed when the forwarding service is not supported.",
"placeholders": {
"servicename": {
@@ -3017,7 +3017,7 @@
"message": "zelfgehost"
},
"thirdParty": {
"message": "van derden"
"message": "Derde partij"
},
"thirdPartyServerMessage": {
"message": "Verbonden met server implementatie van derden, $SERVERNAME$. Reproduceer bugs via de officiële server, of meld ze bij het serverproject.",
@@ -3071,7 +3071,7 @@
"message": "Alle inlogopties bekijken"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Alle inlogopties weergeven"
},
"notificationSentDevice": {
"message": "Er is een melding naar je apparaat verzonden."
@@ -3125,10 +3125,10 @@
"message": "Je organisatiebeleid heeft het automatisch invullen bij laden van pagina ingeschakeld."
},
"howToAutofill": {
"message": "Hoe automatisch aanvullen"
"message": "Hoe automatisch invullen"
},
"autofillSelectInfoWithCommand": {
"message": "Select an item from this screen, use the shortcut $COMMAND$, or explore other options in settings.",
"message": "Selecteer een item in dit scherm, gebruik de sneltoets $COMMAND$ of verken andere opties in de instellingen.",
"placeholders": {
"command": {
"content": "$1",
@@ -3137,16 +3137,16 @@
}
},
"autofillSelectInfoWithoutCommand": {
"message": "Select an item from this screen, or explore other options in settings."
"message": "Selecteer een item in dit scherm of verken andere opties in de instellingen."
},
"gotIt": {
"message": "Ik snap het"
"message": "Oké"
},
"autofillSettings": {
"message": "Instellingen automatisch invullen"
},
"autofillKeyboardShortcutSectionTitle": {
"message": "Shortcut voor automatisch invullen"
"message": "Snelkoppeling automatisch invullen"
},
"autofillKeyboardShortcutUpdateLabel": {
"message": "Snelkoppeling wijzigen"
@@ -3243,7 +3243,7 @@
"message": "Algemeen"
},
"display": {
"message": "Display"
"message": "Weergave"
},
"accountSuccessfullyCreated": {
"message": "Account succesvol aangemaakt!"
@@ -3372,7 +3372,7 @@
"message": "-- Type om te filteren --"
},
"multiSelectLoading": {
"message": "Opties ophalen..."
"message": "Opties ophalen"
},
"multiSelectNotFound": {
"message": "Geen items gevonden"
@@ -3413,7 +3413,7 @@
"description": "Notification button text for starting a fileless import."
},
"importing": {
"message": "Importeren...",
"message": "Importeren",
"description": "Notification message for when an import is in progress."
},
"dataSuccessfullyImported": {
@@ -3432,33 +3432,33 @@
"message": "Aliasdomein"
},
"passwordRepromptDisabledAutofillOnPageLoad": {
"message": "Items with master password re-prompt cannot be autofilled on page load. Autofill on page load turned off.",
"message": "Items met hoofdwachtwoord re-prompt kunnen niet automatisch worden ingevuld bij het laden van de pagina. Automatisch invullen bij laden van pagina uitgeschakeld.",
"description": "Toast message for describing that master password re-prompt cannot be autofilled on page load."
},
"autofillOnPageLoadSetToDefault": {
"message": "Autofill on page load set to use default setting.",
"message": "Automatisch invullen bij het laden van een pagina ingesteld op de standaardinstelling.",
"description": "Toast message for informing the user that autofill on page load has been set to the default setting."
},
"turnOffMasterPasswordPromptToEditField": {
"message": "Turn off master password re-prompt to edit this field",
"message": "Hoofdwachtwoord herhaalprompt uitschakelen om dit veld te bewerken",
"description": "Message appearing below the autofill on load message when master password reprompt is set for a vault item."
},
"toggleSideNavigation": {
"message": "Zijnavigatie schakelen"
},
"skipToContent": {
"message": "Skip to content"
"message": "Overslaan naar inhoud"
},
"bitwardenOverlayButton": {
"message": "Menuknop Bitwarden automatisch invullen",
"description": "Page title for the iframe containing the overlay button"
},
"toggleBitwardenVaultOverlay": {
"message": "Bitwarden auto-invulmenu in- en uitschakelen",
"message": "Bitwarden automatisch invullen menu in- en uitschakelen",
"description": "Screen reader and tool tip label for the overlay button"
},
"bitwardenVault": {
"message": "Bitwarden auto-invulmenu",
"message": "Bitwarden automatisch invullen menu",
"description": "Page title in overlay"
},
"unlockYourAccountToViewMatchingLogins": {
@@ -3486,7 +3486,7 @@
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
"message": "Inloggegevens invullen voor",
"message": "Referenties invullen voor",
"description": "Screen reader text for when overlay item is in focused"
},
"partialUsername": {
@@ -3530,7 +3530,7 @@
"description": "Screen reader text (aria-label) for new identity button within inline menu"
},
"bitwardenOverlayMenuAvailable": {
"message": "Bitwarden auto-invulmenu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.",
"message": "Bitwarden automatisch invullen menu beschikbaar. Druk op de pijltjestoets omlaag om te selecteren.",
"description": "Screen reader text for announcing when the overlay opens on the page"
},
"turnOn": {
@@ -3574,7 +3574,7 @@
"message": "Deze actie vereist verificatie actie. Stel een pincode in om door te gaan."
},
"setPin": {
"message": "Pincode instellen"
"message": "PIN instellen"
},
"verifyWithBiometrics": {
"message": "Biometrisch ontgrendelen"
@@ -3619,7 +3619,7 @@
"message": "Fout bij het verbinden met de Duo-service. Gebruik een andere tweestapsaanmeldingsmethode of neem contact op met Duo voor hulp."
},
"launchDuoAndFollowStepsToFinishLoggingIn": {
"message": "Launch Duo and follow the steps to finish logging in."
"message": "Start Duo en volg de stappen om het inloggen te voltooien."
},
"duoRequiredForAccount": {
"message": "Jouw account vereist Duo-tweestapsaanmelding."
@@ -3628,10 +3628,10 @@
"message": "Open de extensie om in te loggen."
},
"popoutExtension": {
"message": "Popout extension"
"message": "Pop-out extensie"
},
"launchDuo": {
"message": "Launch Duo"
"message": "Duo starten"
},
"importFormatError": {
"message": "De gegevens zijn niet correct opgemaakt. Controleer je importbestand en probeer het opnieuw."
@@ -3759,7 +3759,7 @@
"message": "Kies een passkey om mee in te loggen"
},
"passkeyItem": {
"message": "Passkey-Item"
"message": "Passkey item"
},
"overwritePasskey": {
"message": "Passkey overschrijven?"
@@ -3801,7 +3801,7 @@
"message": "LastPass Email"
},
"importingYourAccount": {
"message": "Account impoteren..."
"message": "Account impoteren"
},
"lastPassMFARequired": {
"message": "LastPass multifactor-authenticatie vereist"
@@ -3825,10 +3825,10 @@
"message": "Wacht op SSO-authenticatie"
},
"awaitingSSODesc": {
"message": "Ga door met inloggen met de inloggegevens van je bedrijf."
"message": "Ga door met inloggen met de referenties van je bedrijf."
},
"seeDetailedInstructions": {
"message": "See detailed instructions on our help site at",
"message": "Zie gedetailleerde instructies op onze helpsite op",
"description": "This is followed a by a hyperlink to the help website."
},
"importDirectlyFromLastPass": {
@@ -3844,52 +3844,52 @@
"message": "Collectie"
},
"lastPassYubikeyDesc": {
"message": "Insert the YubiKey associated with your LastPass account into your computer's USB port, then touch its button."
"message": "Steek de YubiKey die bij je LastPass account hoort in de USB poort van je computer en druk dan op de knop."
},
"switchAccount": {
"message": "Switch account"
"message": "Account wisselen"
},
"switchAccounts": {
"message": "Switch accounts"
"message": "Accounts wisselen"
},
"switchToAccount": {
"message": "Switch to account"
"message": "Wisselen naar account"
},
"activeAccount": {
"message": "Active account"
"message": "Actief account"
},
"availableAccounts": {
"message": "Beschikbare accounts"
},
"accountLimitReached": {
"message": "Account limit reached. Log out of an account to add another."
"message": "Accountlimiet bereikt. Log uit bij een account om een andere toe te voegen."
},
"active": {
"message": "active"
"message": "actief"
},
"locked": {
"message": "locked"
"message": "vergrendeld"
},
"unlocked": {
"message": "unlocked"
"message": "ontgrendeld"
},
"server": {
"message": "server"
},
"hostedAt": {
"message": "hosted at"
"message": "gehost op"
},
"useDeviceOrHardwareKey": {
"message": "Use your device or hardware key"
"message": "Gebruik je apparaat of hardwaresleutel"
},
"justOnce": {
"message": "Just once"
"message": "Eenmalig"
},
"alwaysForThisSite": {
"message": "Always for this site"
"message": "Altijd voor deze site"
},
"domainAddedToExcludedDomains": {
"message": "$DOMAIN$ added to excluded domains.",
"message": "$DOMAIN$ toegevoegd aan uitgesloten domeinen.",
"placeholders": {
"domain": {
"content": "$1",
@@ -3950,7 +3950,7 @@
"description": "Button text for the setting that allows overriding the default browser autofill settings"
},
"saveCipherAttemptSuccess": {
"message": "Credentials saved successfully!",
"message": "Referenties succesvol opgeslagen!",
"description": "Notification message for when saving credentials has succeeded."
},
"passwordSaved": {
@@ -3958,7 +3958,7 @@
"description": "Notification message for when saving credentials has succeeded."
},
"updateCipherAttemptSuccess": {
"message": "Credentials updated successfully!",
"message": "Referenties succesvol bijgewerkt!",
"description": "Notification message for when updating credentials has succeeded."
},
"passwordUpdated": {
@@ -3966,7 +3966,7 @@
"description": "Notification message for when updating credentials has succeeded."
},
"saveCipherAttemptFailed": {
"message": "Error saving credentials. Check console for details.",
"message": "Fout bij het opslaan van referenties. Controleer console voor details.",
"description": "Notification message for when saving credentials has failed."
},
"success": {
@@ -3979,7 +3979,7 @@
"message": "Passkey verwijderd"
},
"autofillSuggestions": {
"message": "Suggesties voor automatisch invullen"
"message": "Suggesties automatisch invullen"
},
"autofillSuggestionsTip": {
"message": "Inlogitem opslaan voor automatisch invullen op deze site"
@@ -3994,7 +3994,7 @@
"message": "Wis filters of probeer een andere zoekterm"
},
"copyInfoTitle": {
"message": "Copy info - $ITEMNAME$",
"message": "Info kopiëren - $ITEMNAME$",
"description": "Title for a button that opens a menu with options to copy information from an item.",
"placeholders": {
"itemname": {
@@ -4004,7 +4004,7 @@
}
},
"copyNoteTitle": {
"message": "Copy Note - $ITEMNAME$",
"message": "Notitie kopiëren - $ITEMNAME$",
"description": "Title for a button copies a note to the clipboard.",
"placeholders": {
"itemname": {
@@ -4014,7 +4014,7 @@
}
},
"moreOptionsLabel": {
"message": "More options, $ITEMNAME$",
"message": "Meer opties, $ITEMNAME$",
"description": "Aria label for a button that opens a menu with more options for an item.",
"placeholders": {
"itemname": {
@@ -4024,7 +4024,7 @@
}
},
"moreOptionsTitle": {
"message": "More options - $ITEMNAME$",
"message": "Meer opties - $ITEMNAME$",
"description": "Title for a button that opens a menu with more options for an item.",
"placeholders": {
"itemname": {
@@ -4034,7 +4034,7 @@
}
},
"viewItemTitle": {
"message": "View item - $ITEMNAME$",
"message": "Item bekijken - $ITEMNAME$",
"description": "Title for a link that opens a view for an item.",
"placeholders": {
"itemname": {
@@ -4060,22 +4060,22 @@
"message": "Aan collecties toewijzen"
},
"copyEmail": {
"message": "Copy email"
"message": "E-mail kopiëren"
},
"copyPhone": {
"message": "Copy phone"
"message": "Telefoon kopiëren"
},
"copyAddress": {
"message": "Copy address"
"message": "Adres kopiëren"
},
"adminConsole": {
"message": "Admin Console"
"message": "Beheerconsole"
},
"accountSecurity": {
"message": "Accountbeveiliging"
},
"notifications": {
"message": "Notifications"
"message": "Meldingen"
},
"appearance": {
"message": "Uiterlijk"
@@ -4107,7 +4107,7 @@
}
},
"new": {
"message": "New"
"message": "Nieuw"
},
"removeItem": {
"message": "Verwijder $NAME$",
@@ -4245,7 +4245,7 @@
"description": "Used within the inline menu to provide an aria description when users are attempting to fill a card cipher."
},
"loginCredentials": {
"message": "Inloggegevens"
"message": "Login referenties"
},
"authenticatorKey": {
"message": "Authenticatiesleutel"
@@ -4546,10 +4546,10 @@
"message": "Itemlocatie"
},
"fileSend": {
"message": "Bestand-Sends"
"message": "Bestand verzenden"
},
"fileSends": {
"message": "Bestand-Sends"
"message": "Bestanden verzenden"
},
"textSend": {
"message": "Tekst-Sends"
@@ -4567,7 +4567,7 @@
"message": "Accountacties"
},
"showNumberOfAutofillSuggestions": {
"message": "Aantal login-autofill-suggesties op het extensie-pictogram weergeven"
"message": "Aantal login automatisch invullen suggesties op het extensie-pictogram weergeven"
},
"showQuickCopyActions": {
"message": "Toon snelle kopieeracties in de kluis"

View File

@@ -1005,7 +1005,7 @@
"message": "Pokaż elementy tożsamości na stronie głównej, aby ułatwić autouzupełnianie."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Kliknij na dane logowania, aby autouzupełnić w widoku Sejfu"
},
"clearClipboard": {
"message": "Wyczyść schowek",

View File

@@ -803,7 +803,7 @@
"message": "Código de verificação inválido"
},
"valueCopied": {
"message": "$VALUE$ copiado",
"message": "$VALUE$ copiado(a)",
"description": "Value has been copied to the clipboard.",
"placeholders": {
"value": {

View File

@@ -193,10 +193,10 @@
"message": "Ауто-пуњење идентитета"
},
"fillVerificationCode": {
"message": "Fill verification code"
"message": "Пуни верификациони кôд"
},
"fillVerificationCodeAria": {
"message": "Fill Verification Code",
"message": "Пуни верификациони кôд",
"description": "Aria label for the heading displayed the inline menu for totp code autofill"
},
"generatePasswordCopied": {
@@ -1005,7 +1005,7 @@
"message": "Прикажи ставке идентитета на страници за лакше аутоматско допуњавање."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Кликните на ставке за ауто-попуњавање у приказу сефа"
},
"clearClipboard": {
"message": "Обриши привремену меморију",
@@ -3071,7 +3071,7 @@
"message": "Погледајте сав извештај у опције"
},
"viewAllLoginOptionsV1": {
"message": "View all log in options"
"message": "Погледајте сав извештај у опције"
},
"notificationSentDevice": {
"message": "Обавештење је послато на ваш уређај."
@@ -3478,11 +3478,11 @@
"description": "Screen reader text (aria-label) for unlock account button in overlay"
},
"totpCodeAria": {
"message": "Time-based One-Time Password Verification Code",
"message": "Једнократни верификациони кôд заснован на времену",
"description": "Aria label for the totp code displayed in the inline menu for autofill"
},
"totpSecondsSpanAria": {
"message": "Time remaining before current TOTP expires",
"message": "Преостало време до истека актуелног ТОТП-а",
"description": "Aria label for the totp seconds displayed in the inline menu for autofill"
},
"fillCredentialsFor": {
@@ -4570,7 +4570,7 @@
"message": "Прикажи број предлога за ауто-попуњавање пријаве на икони додатка"
},
"showQuickCopyActions": {
"message": "Show quick copy actions on Vault"
"message": "Приказати брзе радње копирања у Сефу"
},
"systemDefault": {
"message": "Системски подразумевано"
@@ -4804,22 +4804,22 @@
"message": "Бета"
},
"importantNotice": {
"message": "Important notice"
"message": "Важно обавештење"
},
"setupTwoStepLogin": {
"message": "Set up two-step login"
"message": "Поставити дво-степенску пријаву"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "Bitwarden will send a code to your account email to verify logins from new devices starting in February 2025."
"message": "Bitwarden ће послати кôд на имејл вашег налога за верификовање пријављивања са нових уређаја почевши од фебруара 2025."
},
"newDeviceVerificationNoticeContentPage2": {
"message": "You can set up two-step login as an alternative way to protect your account or change your email to one you can access."
"message": "Можете да подесите пријаву у два корака као алтернативни начин да заштитите свој налог или да промените свој имејл у један који можете да приступите."
},
"remindMeLater": {
"message": "Remind me later"
"message": "Подсети ме касније"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "Do you have reliable access to your email, $EMAIL$?",
"message": "Да ли имате поуздан приступ својим имејлом, $EMAIL$?",
"placeholders": {
"email": {
"content": "$1",
@@ -4828,16 +4828,16 @@
}
},
"newDeviceVerificationNoticePageOneEmailAccessNo": {
"message": "No, I do not"
"message": "Не, ненам"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "Yes, I can reliably access my email"
"message": "Да, могу поуздано да приступим овим имејлом"
},
"turnOnTwoStepLogin": {
"message": "Turn on two-step login"
"message": "Упалити дво-степенску пријаву"
},
"changeAcctEmail": {
"message": "Change account email"
"message": "Променити имејл налога"
},
"extensionWidth": {
"message": "Ширина додатка"

View File

@@ -4154,7 +4154,7 @@
"message": "Ek bilgiler"
},
"itemHistory": {
"message": "Öğe geçmişi"
"message": "Kayıt geçmişi"
},
"lastEdited": {
"message": "Son düzenlenme"

View File

@@ -1005,7 +1005,7 @@
"message": "Показувати список посвідчень на сторінці вкладки для легкого автозаповнення."
},
"clickToAutofillOnVault": {
"message": "Click items to autofill on Vault view"
"message": "Натисніть на запис у режимі перегляду сховища для автозаповнення"
},
"clearClipboard": {
"message": "Очистити буфер обміну",

View File

@@ -284,7 +284,7 @@
"message": "前往网页 App 吗?"
},
"continueToWebAppDesc": {
"message": "在网页应用上探索 Bitwarden 账户的更多功能。"
"message": "在网页 App 上探索 Bitwarden 账户的更多功能。"
},
"continueToHelpCenter": {
"message": "前往帮助中心吗?"
@@ -299,7 +299,7 @@
"message": "帮助别人了解 Bitwarden 是否适合他们。立即访问浏览器的扩展程序商店并留下评分。"
},
"changeMasterPasswordOnWebConfirmation": {
"message": "您可以在 Bitwarden 网页应用上更改您的主密码。"
"message": "您可以在 Bitwarden 网页 App 上更改您的主密码。"
},
"fingerprintPhrase": {
"message": "指纹短语",
@@ -428,7 +428,7 @@
"description": "Short for 'credential generator'."
},
"passGenInfo": {
"message": "自动生成安全可靠唯一的登录密码。"
"message": "自动为您的登录生成强大且唯一的密码。"
},
"bitWebVaultApp": {
"message": "Bitwarden 网页 App"
@@ -597,7 +597,7 @@
"message": "前往"
},
"launchWebsite": {
"message": "启动网站"
"message": "打开网站"
},
"launchWebsiteName": {
"message": "前往 $ITEMNAME$ 的网站",
@@ -763,7 +763,7 @@
"message": "必须填写确认主密码。"
},
"masterPasswordMinlength": {
"message": "主密码必须至少 $VALUE$ 个字符长度。",
"message": "主密码长度必须至少 $VALUE$ 个字符。",
"description": "The Master Password must be at least a specific number of characters long.",
"placeholders": {
"value": {
@@ -888,7 +888,7 @@
"message": "两步登录要求您从其他设备(例如安全钥匙、验证器 App、短信、电话或者电子邮件来验证您的登录这能使您的账户更加安全。两步登录需要在 bitwarden.com 网页版密码库中设置。现在访问此网站吗?"
},
"twoStepLoginConfirmationContent": {
"message": "通过在 Bitwarden 网页 App 中设置两步登录,可以使您的账户更加安全。"
"message": "在 Bitwarden 网页 App 中设置两步登录,您的账户更加安全。"
},
"twoStepLoginConfirmationTitle": {
"message": "前往网页 App 吗?"
@@ -1071,7 +1071,7 @@
"message": "主题"
},
"themeDesc": {
"message": "更改应用程序的颜色主题。"
"message": "更改应用程序的颜色主题。"
},
"themeDescAlt": {
"message": "更改应用程序的颜色主题。适用于所有已登录的账户。"
@@ -1133,7 +1133,7 @@
"message": "确认密码库导出"
},
"exportWarningDesc": {
"message": "导出的密码库数据包含未加密格式。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。用完后请立即将其删除。"
"message": "导出包含未加密格式的密码库数据。您不应该通过不安全的渠道(例如电子邮件)来存储或发送导出文件。使用完后请立即将其删除。"
},
"encExportKeyWarningDesc": {
"message": "此导出将使用您账户的加密密钥来加密您的数据。如果您曾经轮换过账户的加密密钥,您应将其重新导出,否则您将无法解密导出的文件。"
@@ -2123,7 +2123,7 @@
"message": "您仍然想要填充此登录信息吗?"
},
"autofillIframeWarning": {
"message": "该表单由不同于您保存的登录 URI 域名托管。选择「确定」自动填充,或选择「取消」停止填充。"
"message": "该表单由您保存的登录 URI 不同的域名托管。选择「确定」继续自动填充,或选择「取消」停止自动填充。"
},
"autofillIframeWarningTip": {
"message": "要防止以后出现此警告,请将此站点的 URI $HOSTNAME$ 保存到您的 Bitwarden 登录项目中。",
@@ -3982,7 +3982,7 @@
"message": "自动填充建议"
},
"autofillSuggestionsTip": {
"message": "保存此站点登录项目用来自动填充"
"message": "此站点保存为登录项目以用于自动填充"
},
"yourVaultIsEmpty": {
"message": "您的密码库是空的"
@@ -4810,7 +4810,7 @@
"message": "设置两步登录"
},
"newDeviceVerificationNoticeContentPage1": {
"message": "从 2025 年 02 月开始Bitwarden 将向您的账户电子邮箱发送一个代码,以验证来自新设备的登录。"
"message": "从 2025 年 02 月起,当有来自新设备的登录时Bitwarden 将向您的账户电子邮箱发送验证码。"
},
"newDeviceVerificationNoticeContentPage2": {
"message": "您可以设置两步登录作为保护账户的替代方法,或将您的电子邮箱更改为您可以访问的电子邮箱。"
@@ -4819,7 +4819,7 @@
"message": "稍后提醒我"
},
"newDeviceVerificationNoticePageOneFormContent": {
"message": "您能可靠地访问您的电子邮箱 $EMAIL$ 吗?",
"message": "您能可正常访问您的电子邮箱 $EMAIL$ 吗?",
"placeholders": {
"email": {
"content": "$1",
@@ -4831,7 +4831,7 @@
"message": "不,我不能"
},
"newDeviceVerificationNoticePageOneEmailAccessYes": {
"message": "是的,我可以可靠地访问我的电子邮箱"
"message": "是的,我可以正常访问我的电子邮箱"
},
"turnOnTwoStepLogin": {
"message": "开启两步登录"

View File

@@ -1,182 +1,78 @@
<ng-container *ngIf="extensionRefreshFlag">
<popup-page [loading]="loading">
<popup-header slot="header" pageTitle="{{ 'accountActions' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<ng-container *ngIf="availableAccounts$ | async as availableAccounts">
<bit-section [disableMargin]="!enableAccountSwitching">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<div *ngIf="availableAccount.isActive" [ngClass]="{ 'tw-mb-6': enableAccountSwitching }">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
<ng-container *ngIf="enableAccountSwitching">
<bit-section-header *ngIf="isFirst">
<h2 bitTypography="h6" class="tw-font-semibold">{{ "availableAccounts" | i18n }}</h2>
</bit-section-header>
<div *ngIf="!availableAccount.isActive">
<auth-account
[account]="availableAccount"
[extensionRefreshFlag]="extensionRefreshFlag"
(loading)="loading = $event"
></auth-account>
</div>
</ng-container>
</ng-container>
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</bit-section>
<popup-page [loading]="loading">
<popup-header slot="header" pageTitle="{{ 'accountActions' | i18n }}" showBackButton>
<ng-container slot="end">
<app-pop-out></app-pop-out>
<app-current-account></app-current-account>
</ng-container>
</popup-header>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<bit-section>
<bit-section-header>
<h2 bitTypography="h6" class="tw-font-semibold">
{{ "options" | i18n }}
</h2>
</bit-section-header>
<ng-container *ngIf="availableAccounts$ | async as availableAccounts">
<bit-section [disableMargin]="!enableAccountSwitching">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<div *ngIf="availableAccount.isActive" [ngClass]="{ 'tw-mb-6': enableAccountSwitching }">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</div>
<bit-item>
<button
type="button"
bit-item-content
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="logOut(currentAccount.id)">
<i
slot="start"
class="bwi bwi-sign-out tw-text-2xl tw-text-main"
aria-hidden="true"
></i>
{{ "logOut" | i18n }}
</button>
</bit-item>
<bit-item *ngIf="showLockAll$ | async">
<button type="button" bit-item-content (click)="lockAll()">
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</bit-item>
</bit-section>
</div>
</popup-page>
</ng-container>
<ng-container *ngIf="enableAccountSwitching">
<bit-section-header *ngIf="isFirst">
<h2 bitTypography="h6" class="tw-font-semibold">{{ "availableAccounts" | i18n }}</h2>
</bit-section-header>
<ng-container *ngIf="!extensionRefreshFlag">
<app-header>
<div class="left">
<button type="button" (click)="back()">{{ "close" | i18n }}</button>
</div>
<div class="center tw-font-bold">{{ "switchAccounts" | i18n }}</div>
</app-header>
<div *ngIf="!availableAccount.isActive">
<auth-account [account]="availableAccount" (loading)="loading = $event"></auth-account>
</div>
</ng-container>
</ng-container>
<main
*ngIf="loading"
class="tw-absolute tw-z-50 tw-box-border tw-flex tw-cursor-not-allowed tw-items-center tw-justify-center tw-bg-background tw-opacity-60"
>
<i class="bwi bwi-spinner bwi-2x bwi-spin" aria-hidden="true"></i>
</main>
<main>
<div class="tw-p-2">
<div *ngIf="availableAccounts$ | async as availableAccounts">
<ul class="tw-grid tw-list-none tw-gap-2" role="listbox">
<ng-container *ngFor="let availableAccount of availableAccounts; first as isFirst">
<li *ngIf="availableAccount.isActive" class="tw-mb-4" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
<ng-container *ngIf="enableAccountSwitching">
<div *ngIf="isFirst" class="tw-uppercase tw-text-muted">
{{ "availableAccounts" | i18n }}
</div>
<li *ngIf="!availableAccount.isActive" role="option">
<auth-account
[account]="availableAccount"
(loading)="loading = $event"
></auth-account>
</li>
</ng-container>
</ng-container>
</ul>
<!--
<!--
If the user has not reached the account limit, the last 'availableAccount' will have an 'id' of
'SPECIAL_ADD_ACCOUNT_ID'. Since we don't want to count this as one of the actual accounts,
we check to make sure the 'id' of the last 'availableAccount' is not equal to 'SPECIAL_ADD_ACCOUNT_ID'
-->
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</div>
<p
class="tw-text-sm tw-text-muted"
*ngIf="
availableAccounts.length >= accountLimit &&
availableAccounts[availableAccounts.length - 1].id !== specialAddAccountId
"
>
{{ "accountLimitReached" | i18n }}
</p>
</bit-section>
</ng-container>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<div class="tw-mb-2 tw-uppercase tw-text-muted">{{ "options" | i18n }}</div>
<div class="tw-grid tw-gap-2">
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3 disabled:tw-cursor-not-allowed disabled:tw-border-text-muted/60 disabled:!tw-text-muted/60"
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="logOut(currentAccount.id)"
>
<i class="bwi bwi-sign-out tw-text-2xl" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
<button
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-p-3"
(click)="lockAll()"
*ngIf="showLockAll$ | async"
>
<i class="bwi bwi-lock tw-text-2xl" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</div>
</div>
</div>
</main>
</ng-container>
<div class="tw-mt-8" *ngIf="currentAccount$ | async as currentAccount">
<bit-section>
<bit-section-header>
<h2 bitTypography="h6" class="tw-font-semibold">
{{ "options" | i18n }}
</h2>
</bit-section-header>
<bit-item>
<button
type="button"
bit-item-content
(click)="lock(currentAccount.id)"
[disabled]="currentAccount.status === lockedStatus || !activeUserCanLock"
[title]="!activeUserCanLock ? ('unlockMethodNeeded' | i18n) : ''"
>
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockNow" | i18n }}
</button>
</bit-item>
<bit-item>
<button type="button" bit-item-content (click)="logOut(currentAccount.id)">
<i slot="start" class="bwi bwi-sign-out tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "logOut" | i18n }}
</button>
</bit-item>
<bit-item *ngIf="showLockAll$ | async">
<button type="button" bit-item-content (click)="lockAll()">
<i slot="start" class="bwi bwi-lock tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ "lockAll" | i18n }}
</button>
</bit-item>
</bit-section>
</div>
</popup-page>

View File

@@ -10,9 +10,7 @@ import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeou
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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { UserId } from "@bitwarden/common/types/guid";
import {
AvatarModule,
@@ -25,7 +23,6 @@ import {
import { enableAccountSwitching } from "../../../platform/flags";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { HeaderComponent } from "../../../platform/popup/header.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
@@ -44,7 +41,6 @@ import { AccountSwitcherService } from "./services/account-switcher.service";
AvatarModule,
PopupPageComponent,
PopupHeaderComponent,
HeaderComponent,
PopOutComponent,
CurrentAccountComponent,
AccountComponent,
@@ -58,7 +54,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
loading = false;
activeUserCanLock = false;
extensionRefreshFlag = false;
enableAccountSwitching = true;
constructor(
@@ -70,7 +65,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
private router: Router,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
private authService: AuthService,
private configService: ConfigService,
private lockService: LockService,
) {}
@@ -109,9 +103,6 @@ export class AccountSwitcherComponent implements OnInit, OnDestroy {
async ngOnInit() {
this.enableAccountSwitching = enableAccountSwitching();
this.extensionRefreshFlag = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
const availableVaultTimeoutActions = await firstValueFrom(
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),

View File

@@ -1,109 +1,44 @@
<ng-container *ngIf="extensionRefreshFlag">
<bit-item *ngIf="account.id !== specialAccountAddId">
<button bit-item-content type="button" (click)="selectAccount(account.id)">
<bit-avatar
slot="start"
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<bit-item *ngIf="account.id !== specialAccountAddId">
<button bit-item-content type="button" (click)="selectAccount(account.id)">
<bit-avatar
slot="start"
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<ng-container slot="secondary">
<div class="tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div class="tw-text-sm tw-italic" [attr.aria-hidden]="status.text === 'active'">
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</ng-container>
<i slot="end" class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</button>
</bit-item>
<bit-item *ngIf="account.id === specialAccountAddId">
<button type="button" bit-item-content (click)="selectAccount(account.id)">
<i slot="start" class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ account.name | i18n }}
</button>
</bit-item>
</ng-container>
<ng-container *ngIf="!extensionRefreshFlag">
<button
*ngIf="account.id !== specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<div class="tw-flex-shrink-0">
<bit-avatar
[id]="account.id"
[text]="account.name"
[color]="account.avatarColor"
size="small"
aria-hidden="true"
></bit-avatar>
<span class="tw-sr-only" *ngIf="status.text === 'active'"> {{ "activeAccount" | i18n }}: </span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="tw-text-left">
<span class="tw-sr-only" *ngIf="status.text === 'active'">
{{ "activeAccount" | i18n }}:
</span>
<span class="tw-sr-only" *ngIf="status.text !== 'active'">
{{ "switchToAccount" | i18n }}
</span>
<div class="tw-max-w-64 tw-truncate">
{{ account.email }}
</div>
<div class="account-switcher-row-details tw-max-w-64 tw-truncate tw-text-sm">
<ng-container slot="secondary">
<div class="tw-max-w-64 tw-truncate tw-text-sm">
<span class="tw-sr-only">{{ "hostedAt" | i18n }}</span>
{{ account.server }}
</div>
<div
class="account-switcher-row-details tw-text-sm tw-italic"
[attr.aria-hidden]="status.text === 'active'"
>
<div class="tw-text-sm tw-italic" [attr.aria-hidden]="status.text === 'active'">
<span class="tw-sr-only">(</span>
<span [ngClass]="status.text === 'active' ? 'tw-font-bold tw-text-success' : ''">{{
status.text
}}</span>
<span class="tw-sr-only">)</span>
</div>
</div>
<div class="tw-ml-auto tw-flex-shrink-0">
<i class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</div>
</button>
</ng-container>
<button
*ngIf="account.id === specialAccountAddId"
type="button"
class="account-switcher-row tw-flex tw-w-full tw-items-center tw-gap-3 tw-rounded-md tw-border-none tw-p-3"
(click)="selectAccount(account.id)"
>
<i class="bwi bwi-plus tw-text-2xl" aria-hidden="true"></i>
<div>
{{ account.name | i18n }}
</div>
<i slot="end" class="bwi tw-text-2xl" [ngClass]="status.icon" aria-hidden="true"></i>
</button>
</ng-container>
</bit-item>
<bit-item *ngIf="account.id === specialAccountAddId">
<button type="button" bit-item-content (click)="selectAccount(account.id)">
<i slot="start" class="bwi bwi-plus tw-text-2xl tw-text-main" aria-hidden="true"></i>
{{ account.name | i18n }}
</button>
</bit-item>

View File

@@ -19,7 +19,6 @@ import { AccountSwitcherService, AvailableAccount } from "./services/account-swi
})
export class AccountComponent {
@Input() account: AvailableAccount;
@Input() extensionRefreshFlag: boolean = false;
@Output() loading = new EventEmitter<boolean>();
constructor(

View File

@@ -20,6 +20,7 @@
[showReadonlyHostname]="showReadonlyHostname"
[hideLogo]="true"
[maxWidth]="maxWidth"
[hideFooter]="hideFooter"
>
<router-outlet></router-outlet>
<router-outlet slot="secondary" name="secondary"></router-outlet>

View File

@@ -25,6 +25,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
showAcctSwitcher?: boolean;
showBackButton?: boolean;
showLogo?: boolean;
hideFooter?: boolean;
}
@Component({
@@ -54,6 +55,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
protected showReadonlyHostname: boolean;
protected maxWidth: "md" | "3xl";
protected hasLoggedInAccount: boolean = false;
protected hideFooter: boolean;
protected theme: string;
protected logo = ExtensionBitwardenLogo;
@@ -112,6 +114,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.pageIcon = firstChildRouteData["pageIcon"];
}
this.hideFooter = Boolean(firstChildRouteData["hideFooter"]);
this.showReadonlyHostname = Boolean(firstChildRouteData["showReadonlyHostname"]);
this.maxWidth = firstChildRouteData["maxWidth"];
@@ -158,6 +161,10 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.pageIcon = data.pageIcon !== null ? data.pageIcon : null;
}
if (data.hideFooter !== undefined) {
this.hideFooter = data.hideFooter !== null ? data.hideFooter : null;
}
if (data.showReadonlyHostname !== undefined) {
this.showReadonlyHostname = data.showReadonlyHostname;
}
@@ -194,6 +201,7 @@ export class ExtensionAnonLayoutWrapperComponent implements OnInit, OnDestroy {
this.showBackButton = null;
this.showLogo = null;
this.maxWidth = null;
this.hideFooter = null;
}
ngOnDestroy() {

View File

@@ -1,140 +0,0 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "accountSecurity" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1" [formGroup]="form">
<div class="box list">
<h2 class="box-header">{{ "unlockMethods" | i18n }}</h2>
<div class="box-content single-line">
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="supportsBiometric">
<label for="biometric">{{ "unlockWithBiometrics" | i18n }}</label>
<input id="biometric" type="checkbox" formControlName="biometric" />
</div>
<div
class="box-content-row box-content-row-checkbox"
appBoxRow
*ngIf="supportsBiometric && this.form.value.biometric"
>
<label for="autoBiometricsPrompt">{{ "enableAutoBiometricsPrompt" | i18n }}</label>
<input
id="autoBiometricsPrompt"
type="checkbox"
(change)="updateAutoBiometricsPrompt()"
formControlName="enableAutoBiometricsPrompt"
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="pin">{{ "unlockWithPin" | i18n }}</label>
<input id="pin" type="checkbox" formControlName="pin" />
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "sessionTimeoutHeader" | i18n }}</h2>
<div class="box-content single-line">
<app-callout type="info" *ngIf="vaultTimeoutPolicyCallout | async as policy">
<span *ngIf="policy.timeout && policy.action">
{{
"vaultTimeoutPolicyWithActionInEffect"
| i18n: policy.timeout.hours : policy.timeout.minutes : (policy.action | i18n)
}}
</span>
<span *ngIf="policy.timeout && !policy.action">
{{ "vaultTimeoutPolicyInEffect" | i18n: policy.timeout.hours : policy.timeout.minutes }}
</span>
<span *ngIf="!policy.timeout && policy.action">
{{ "vaultTimeoutActionPolicyInEffect" | i18n: (policy.action | i18n) }}
</span>
</app-callout>
<app-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</app-vault-timeout-input>
<div class="box-content-row display-block" appBoxRow>
<label for="vaultTimeoutAction">{{ "vaultTimeoutAction" | i18n }}</label>
<select
id="vaultTimeoutAction"
name="VaultTimeoutActions"
formControlName="vaultTimeoutAction"
>
<option *ngFor="let action of availableVaultTimeoutActions" [ngValue]="action">
{{ action | i18n }}
</option>
</select>
</div>
<div
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
id="unlockMethodHelp"
class="box-footer"
>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}
</div>
</div>
</div>
<div class="box list">
<h2 class="box-header">{{ "otherOptions" | i18n }}</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="fingerprint()"
>
<div class="row-main">{{ "fingerprintPhrase" | i18n }}</div>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="twoStep()"
>
<div class="row-main">{{ "twoStepLogin" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="changePassword()"
*ngIf="showChangeMasterPass"
>
<div class="row-main">{{ "changeMasterPassword" | i18n }}</div>
<i class="bwi bwi-external-link bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="
!accountSwitcherEnabled && availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)
"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="lock()"
>
<div class="row-main">{{ "lockNow" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
*ngIf="!accountSwitcherEnabled"
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="logOut()"
>
<div class="row-main">{{ "logOut" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@@ -1,499 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import {
BehaviorSubject,
combineLatest,
concatMap,
distinctUntilChanged,
filter,
firstValueFrom,
map,
Observable,
pairwise,
startWith,
Subject,
switchMap,
takeUntil,
} from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/types/vault-timeout.type";
import { DialogService } from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsService } from "@bitwarden/key-management";
import { BiometricErrors, BiometricErrorTypes } from "../../../models/biometricErrors";
import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { SetPinComponent } from "../components/set-pin.component";
import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
@Component({
selector: "auth-account-security",
templateUrl: "account-security-v1.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AccountSecurityComponent implements OnInit, OnDestroy {
protected readonly VaultTimeoutAction = VaultTimeoutAction;
availableVaultTimeoutActions: VaultTimeoutAction[] = [];
vaultTimeoutOptions: VaultTimeoutOption[];
vaultTimeoutPolicyCallout: Observable<{
timeout: { hours: string; minutes: string };
action: VaultTimeoutAction;
}>;
supportsBiometric: boolean;
showChangeMasterPass = true;
accountSwitcherEnabled = false;
form = this.formBuilder.group({
vaultTimeout: [null as VaultTimeout | null],
vaultTimeoutAction: [VaultTimeoutAction.Lock],
pin: [null as boolean | null],
biometric: false,
enableAutoBiometricsPrompt: true,
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService,
private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private vaultTimeoutService: VaultTimeoutService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
public messagingService: MessagingService,
private environmentService: EnvironmentService,
private keyService: KeyService,
private stateService: StateService,
private userVerificationService: UserVerificationService,
private dialogService: DialogService,
private changeDetectorRef: ChangeDetectorRef,
private biometricStateService: BiometricStateService,
private biometricsService: BiometricsService,
) {
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
const maximumVaultTimeoutPolicy = this.policyService.get$(PolicyType.MaximumVaultTimeout);
this.vaultTimeoutPolicyCallout = maximumVaultTimeoutPolicy.pipe(
filter((policy) => policy != null),
map((policy) => {
let timeout;
if (policy.data?.minutes) {
timeout = {
hours: Math.floor(policy.data?.minutes / 60).toString(),
minutes: (policy.data?.minutes % 60).toString(),
};
}
return { timeout: timeout, action: policy.data?.action };
}),
);
const showOnLocked =
!this.platformUtilsService.isFirefox() && !this.platformUtilsService.isSafari();
this.vaultTimeoutOptions = [
{ name: this.i18nService.t("immediately"), value: 0 },
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
];
if (showOnLocked) {
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
this.vaultTimeoutOptions.push({
name: this.i18nService.t("onRestart"),
value: VaultTimeoutStringType.OnRestart,
});
this.vaultTimeoutOptions.push({
name: this.i18nService.t("never"),
value: VaultTimeoutStringType.Never,
});
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
let timeout = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
);
if (timeout === VaultTimeoutStringType.OnLocked && !showOnLocked) {
timeout = VaultTimeoutStringType.OnRestart;
}
const initialValues = {
vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
),
pin: await this.pinService.isPinSet(activeAccount.id),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$,
),
};
this.form.patchValue(initialValues, { emitEvent: false });
this.supportsBiometric = await this.biometricsService.supportsBiometric();
this.showChangeMasterPass = await this.userVerificationService.hasMasterPassword();
this.form.controls.vaultTimeout.valueChanges
.pipe(
startWith(initialValues.vaultTimeout), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeout(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.vaultTimeoutAction.valueChanges
.pipe(
startWith(initialValues.vaultTimeoutAction), // emit to init pairwise
pairwise(),
concatMap(async ([previousValue, newValue]) => {
await this.saveVaultTimeoutAction(previousValue, newValue);
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.pin.valueChanges
.pipe(
concatMap(async (value) => {
await this.updatePin(value);
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.form.controls.biometric.valueChanges
.pipe(
distinctUntilChanged(),
concatMap(async (enabled) => {
await this.updateBiometric(enabled);
if (enabled) {
this.form.controls.enableAutoBiometricsPrompt.enable();
} else {
this.form.controls.enableAutoBiometricsPrompt.disable();
}
this.refreshTimeoutSettings$.next();
}),
takeUntil(this.destroy$),
)
.subscribe();
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, action]) => {
this.availableVaultTimeoutActions = availableActions;
this.form.controls.vaultTimeoutAction.setValue(action, { emitEvent: false });
// NOTE: The UI doesn't properly update without detect changes.
// I've even tried using an async pipe, but it still doesn't work. I'm not sure why.
// Using an async pipe means that we can't call `detectChanges` AFTER the data has change
// meaning that we are forced to use regular class variables instead of observables.
this.changeDetectorRef.detectChanges();
});
this.refreshTimeoutSettings$
.pipe(
switchMap(() =>
combineLatest([
this.vaultTimeoutSettingsService.availableVaultTimeoutActions$(),
maximumVaultTimeoutPolicy,
]),
),
takeUntil(this.destroy$),
)
.subscribe(([availableActions, policy]) => {
if (policy?.data?.action || availableActions.length <= 1) {
this.form.controls.vaultTimeoutAction.disable({ emitEvent: false });
} else {
this.form.controls.vaultTimeoutAction.enable({ emitEvent: false });
}
});
}
async saveVaultTimeout(previousValue: VaultTimeout, newValue: VaultTimeout) {
if (newValue === VaultTimeoutStringType.Never) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "warning" },
content: { key: "neverLockWarning" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeout.setValue(previousValue, { emitEvent: false });
return;
}
}
// The minTimeoutError does not apply to browser because it supports Immediately
// So only check for the policyError
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
const vaultTimeoutAction = await firstValueFrom(
this.vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$(activeAccount.id),
);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
newValue,
vaultTimeoutAction,
);
if (newValue === VaultTimeoutStringType.Never) {
this.messagingService.send("bgReseedStorage");
}
}
async saveVaultTimeoutAction(previousValue: VaultTimeoutAction, newValue: VaultTimeoutAction) {
if (newValue === VaultTimeoutAction.LogOut) {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "vaultTimeoutLogOutConfirmationTitle" },
content: { key: "vaultTimeoutLogOutConfirmation" },
type: "warning",
});
if (!confirmed) {
this.form.controls.vaultTimeoutAction.setValue(previousValue, {
emitEvent: false,
});
return;
}
}
if (this.form.controls.vaultTimeout.hasError("policyError")) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("vaultTimeoutTooLarge"),
);
return;
}
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
await this.vaultTimeoutSettingsService.setVaultTimeoutOptions(
activeAccount.id,
this.form.value.vaultTimeout,
newValue,
);
this.refreshTimeoutSettings$.next();
}
async updatePin(value: boolean) {
if (value) {
const dialogRef = SetPinComponent.open(this.dialogService);
if (dialogRef == null) {
this.form.controls.pin.setValue(false, { emitEvent: false });
return;
}
const userHasPinSet = await firstValueFrom(dialogRef.closed);
this.form.controls.pin.setValue(userHasPinSet, { emitEvent: false });
} else {
await this.vaultTimeoutSettingsService.clear();
}
}
async updateBiometric(enabled: boolean) {
if (enabled && this.supportsBiometric) {
let granted;
try {
granted = await BrowserApi.requestPermission({ permissions: ["nativeMessaging"] });
} catch (e) {
// eslint-disable-next-line
console.error(e);
if (this.platformUtilsService.isFirefox() && BrowserPopupUtils.inSidebar(window)) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionSidebarTitle" },
content: { key: "nativeMessaginPermissionSidebarDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "info",
});
this.form.controls.biometric.setValue(false);
return;
}
}
if (!granted) {
await this.dialogService.openSimpleDialog({
title: { key: "nativeMessaginPermissionErrorTitle" },
content: { key: "nativeMessaginPermissionErrorDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
this.form.controls.biometric.setValue(false);
return;
}
const awaitDesktopDialogRef = AwaitDesktopDialogComponent.open(this.dialogService);
const awaitDesktopDialogClosed = firstValueFrom(awaitDesktopDialogRef.closed);
await this.keyService.refreshAdditionalKeys();
await Promise.race([
awaitDesktopDialogClosed.then(async (result) => {
if (result !== true) {
this.form.controls.biometric.setValue(false);
}
}),
this.biometricsService
.authenticateBiometric()
.then((result) => {
this.form.controls.biometric.setValue(result);
if (!result) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorEnableBiometricTitle"),
this.i18nService.t("errorEnableBiometricDesc"),
);
}
})
.catch((e) => {
// Handle connection errors
this.form.controls.biometric.setValue(false);
const error = BiometricErrors[e.message as BiometricErrorTypes];
// 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.dialogService.openSimpleDialog({
title: { key: error.title },
content: { key: error.description },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "danger",
});
})
.finally(() => {
awaitDesktopDialogRef.close(true);
}),
]);
} else {
await this.biometricStateService.setBiometricUnlockEnabled(false);
await this.biometricStateService.setFingerprintValidated(false);
}
}
async updateAutoBiometricsPrompt() {
await this.biometricStateService.setPromptAutomatically(
this.form.value.enableAutoBiometricsPrompt,
);
}
async changePassword() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "continueToWebApp" },
content: { key: "changeMasterPasswordOnWebConfirmation" },
type: "info",
acceptButtonText: { key: "continue" },
});
if (confirmed) {
const env = await firstValueFrom(this.environmentService.environment$);
await BrowserApi.createNewTab(env.getWebVaultUrl());
}
}
async twoStep() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "twoStepLogin" },
content: { key: "twoStepLoginConfirmation" },
type: "info",
});
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab("https://bitwarden.com/help/setup-two-step-login/");
}
}
async fingerprint() {
const fingerprint = await this.keyService.getFingerprint(await this.stateService.getUserId());
const dialogRef = FingerprintDialogComponent.open(this.dialogService, {
fingerprint,
});
return firstValueFrom(dialogRef.closed);
}
async lock() {
await this.vaultTimeoutService.lock();
}
async logOut() {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "logOut" },
content: { key: "logOutConfirmation" },
type: "info",
});
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (confirmed) {
this.messagingService.send("logout", { userId: userId });
}
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@@ -60,7 +60,6 @@ import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../../platform/flags";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
import { SetPinComponent } from "../components/set-pin.component";
@@ -82,7 +81,6 @@ import { AwaitDesktopDialogComponent } from "./await-desktop-dialog.component";
JslibModule,
LinkModule,
PopOutComponent,
PopupFooterComponent,
PopupHeaderComponent,
PopupPageComponent,
RouterModule,

View File

@@ -60,10 +60,18 @@ describe("NotificationBackground", () => {
const configService = mock<ConfigService>();
const accountService = mock<AccountService>();
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
id: "testId" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
});
beforeEach(() => {
activeAccountStatusMock$ = new BehaviorSubject(AuthenticationStatus.Locked);
authService = mock<AuthService>();
authService.activeAccountStatus$ = activeAccountStatusMock$;
accountService.activeAccount$ = activeAccountSubject;
notificationBackground = new NotificationBackground(
autofillService,
cipherService,
@@ -683,13 +691,6 @@ describe("NotificationBackground", () => {
});
describe("saveOrUpdateCredentials", () => {
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
id: "testId" as UserId,
email: "test@example.com",
emailVerified: true,
name: "Test User",
});
let getDecryptedCipherByIdSpy: jest.SpyInstance;
let getAllDecryptedForUrlSpy: jest.SpyInstance;
let updatePasswordSpy: jest.SpyInstance;

View File

@@ -83,6 +83,8 @@ export default class NotificationBackground {
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
};
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private autofillService: AutofillService,
private cipherService: CipherService,
@@ -569,9 +571,7 @@ export default class NotificationBackground {
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(newCipher, activeUserId);
try {
@@ -611,10 +611,7 @@ export default class NotificationBackground {
return;
}
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(cipherView, activeUserId);
try {
// We've only updated the password, no need to broadcast editedCipher message
@@ -647,17 +644,15 @@ export default class NotificationBackground {
if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false;
}
const folders = await firstValueFrom(this.folderService.folderViews$);
const activeUserId = await firstValueFrom(this.activeUserId$);
const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId));
return folders.some((x) => x.id === folderId);
}
private async getDecryptedCipherById(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
if (cipher != null && cipher.type === CipherType.Login) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
@@ -697,7 +692,8 @@ export default class NotificationBackground {
* Returns the first value found from the folder service's folderViews$ observable.
*/
private async getFolderData() {
return await firstValueFrom(this.folderService.folderViews$);
const activeUserId = await firstValueFrom(this.activeUserId$);
return await firstValueFrom(this.folderService.folderViews$(activeUserId));
}
private async getWebVaultUrl(): Promise<string> {

View File

@@ -2275,6 +2275,7 @@ export class OverlayBackground implements OverlayBackgroundInterface {
card,
identity,
sender,
addNewCipherType,
}: CurrentAddNewItemData) {
const cipherView: CipherView = this.buildNewVaultItemCipherView({
login,
@@ -2294,7 +2295,10 @@ export class OverlayBackground implements OverlayBackgroundInterface {
collectionIds: cipherView.collectionIds,
});
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await this.openAddEditVaultItemPopout(sender.tab, {
cipherId: cipherView.id,
cipherType: addNewCipherType,
});
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");
} catch (error) {
this.logService.error("Error building cipher and opening add/edit vault item popout", error);

View File

@@ -458,7 +458,7 @@ export class BrowserApi {
// and that prompts us to show a new tab, this apparently doesn't happen on sideloaded
// extensions and only shows itself production scenarios. See: https://bitwarden.atlassian.net/browse/PM-12298
if (this.isSafariApi) {
self.location.reload();
return self.location.reload();
}
return chrome.runtime.reload();
}

View File

@@ -1,9 +1,4 @@
<ng-container *ngIf="show && !useRefreshVariant">
<button type="button" (click)="expand()" appA11yTitle="{{ 'popOutNewWindow' | i18n }}">
<i class="bwi bwi-external-link bwi-rotate-270 bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="show && useRefreshVariant">
<ng-container *ngIf="show">
<button
bitIconButton="bwi-popout"
size="small"

View File

@@ -2,8 +2,6 @@ import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { IconButtonModule } from "@bitwarden/components";
@@ -17,16 +15,10 @@ import BrowserPopupUtils from "../browser-popup-utils";
})
export class PopOutComponent implements OnInit {
@Input() show = true;
useRefreshVariant = false;
constructor(
private platformUtilsService: PlatformUtilsService,
private configService: ConfigService,
) {}
constructor(private platformUtilsService: PlatformUtilsService) {}
async ngOnInit() {
this.useRefreshVariant = await this.configService.getFeatureFlag(FeatureFlag.ExtensionRefresh);
if (this.show) {
if (
(BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()) ||

View File

@@ -3,7 +3,6 @@ import { Subject } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@@ -30,12 +29,12 @@ describe("ForegroundSyncService", () => {
const cipherService = mock<CipherService>();
const collectionService = mock<CollectionService>();
const apiService = mock<ApiService>();
const accountService = mock<AccountService>();
const accountService = mockAccountServiceWith(userId);
const authService = mock<AuthService>();
const sendService = mock<InternalSendService>();
const sendApiService = mock<SendApiService>();
const messageListener = mock<MessageListener>();
const stateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
const stateProvider = new FakeStateProvider(accountService);
const sut = new ForegroundSyncService(
stateService,

View File

@@ -65,7 +65,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
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 as AccountSecurityV1Component } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { TwoFactorAuthComponent } from "../auth/popup/two-factor-auth.component";
@@ -94,28 +93,16 @@ import { ExportBrowserV2Component } from "../tools/popup/settings/export/export-
import { ImportBrowserV2Component } from "../tools/popup/settings/import/import-browser-v2.component";
import { SettingsV2Component } from "../tools/popup/settings/settings-v2.component";
import { clearVaultStateGuard } from "../vault/guards/clear-vault-state.guard";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AddEditV2Component } from "../vault/popup/components/vault-v2/add-edit/add-edit-v2.component";
import { AssignCollections } from "../vault/popup/components/vault-v2/assign-collections/assign-collections.component";
import { AttachmentsV2Component } from "../vault/popup/components/vault-v2/attachments/attachments-v2.component";
import { PasswordHistoryV2Component } from "../vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component";
import { VaultV2Component } from "../vault/popup/components/vault-v2/vault-v2.component";
import { ViewV2Component } from "../vault/popup/components/vault-v2/view-v2/view-v2.component";
import { AppearanceV2Component } from "../vault/popup/settings/appearance-v2.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersV2Component } from "../vault/popup/settings/folders-v2.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { TrashComponent } from "../vault/popup/settings/trash.component";
import { VaultSettingsV2Component } from "../vault/popup/settings/vault-settings-v2.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { RouteElevation } from "./app-routing.animations";
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
@@ -272,56 +259,43 @@ const routes: Routes = [
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "ciphers",
component: VaultItemsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(ViewComponent, ViewV2Component, {
path: "view-cipher",
component: ViewV2Component,
canActivate: [authGuard],
data: {
// Above "trash"
elevation: 3,
} satisfies RouteDataProperties,
}),
...extensionRefreshSwap(PasswordHistoryComponent, PasswordHistoryV2Component, {
},
{
path: "cipher-password-history",
component: PasswordHistoryV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
},
{
path: "add-cipher",
component: AddEditV2Component,
canActivate: [authGuard, debounceNavigationGuard()],
data: { elevation: 1 } satisfies RouteDataProperties,
runGuardsAndResolvers: "always",
}),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
},
{
path: "edit-cipher",
component: AddEditV2Component,
canActivate: [authGuard, debounceNavigationGuard()],
data: {
// Above "trash"
elevation: 3,
} satisfies RouteDataProperties,
runGuardsAndResolvers: "always",
}),
{
path: "share-cipher",
component: ShareComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "collections",
component: CollectionsComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(AttachmentsComponent, AttachmentsV2Component, {
path: "attachments",
component: AttachmentsV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "generator",
component: CredentialGeneratorComponent,
@@ -351,43 +325,28 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(AccountSecurityV1Component, AccountSecurityComponent, {
{
path: "account-security",
component: AccountSecurityComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
...extensionRefreshSwap(NotificationsSettingsV1Component, NotificationsSettingsComponent, {
path: "notifications",
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(VaultSettingsComponent, VaultSettingsV2Component, {
{
path: "vault-settings",
component: VaultSettingsV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(FoldersComponent, FoldersV2Component, {
},
{
path: "folders",
component: FoldersV2Component,
canActivate: [authGuard],
data: { elevation: 2 } satisfies RouteDataProperties,
}),
{
path: "add-folder",
component: FolderAddEditComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "edit-folder",
component: FolderAddEditComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
path: "sync",
component: SyncComponent,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(ExcludedDomainsV1Component, ExcludedDomainsComponent, {
path: "excluded-domains",
@@ -400,16 +359,18 @@ const routes: Routes = [
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
...extensionRefreshSwap(AppearanceComponent, AppearanceV2Component, {
{
path: "appearance",
component: AppearanceV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
},
{
path: "clone-cipher",
component: AddEditV2Component,
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
}),
},
{
path: "add-send",
component: SendAddEditV2Component,
@@ -685,7 +646,7 @@ const routes: Routes = [
{
path: "assign-collections",
component: AssignCollections,
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh, true, "/")],
canActivate: [authGuard],
data: { elevation: 1 } satisfies RouteDataProperties,
},
{
@@ -713,6 +674,7 @@ const routes: Routes = [
pageTitle: {
key: "importantNotice",
},
hideFooter: true,
},
},
{

View File

@@ -29,7 +29,6 @@ import { LoginViaAuthRequestComponentV1 } from "../auth/popup/login-via-auth-req
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 as AccountSecurityComponentV1 } from "../auth/popup/settings/account-security-v1.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
@@ -55,25 +54,6 @@ import { PopupHeaderComponent } from "../platform/popup/layout/popup-header.comp
import { PopupPageComponent } from "../platform/popup/layout/popup-page.component";
import { PopupTabNavigationComponent } from "../platform/popup/layout/popup-tab-navigation.component";
import { FilePopoutCalloutComponent } from "../tools/popup/components/file-popout-callout.component";
import { ActionButtonsComponent } from "../vault/popup/components/action-buttons.component";
import { CipherRowComponent } from "../vault/popup/components/cipher-row.component";
import { AddEditCustomFieldsComponent } from "../vault/popup/components/vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/popup/components/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/popup/components/vault/attachments.component";
import { CollectionsComponent } from "../vault/popup/components/vault/collections.component";
import { CurrentTabComponent } from "../vault/popup/components/vault/current-tab.component";
import { PasswordHistoryComponent } from "../vault/popup/components/vault/password-history.component";
import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component";
import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.component";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
@@ -128,49 +108,29 @@ import "../platform/popup/locales";
ExtensionAnonLayoutWrapperComponent,
],
declarations: [
ActionButtonsComponent,
AddEditComponent,
AddEditCustomFieldsComponent,
AppComponent,
AttachmentsComponent,
CipherRowComponent,
VaultItemsComponent,
CollectionsComponent,
ColorPasswordPipe,
ColorPasswordCountPipe,
CurrentTabComponent,
EnvironmentComponent,
ExcludedDomainsV1Component,
Fido2CipherRowV1Component,
Fido2UseBrowserLinkV1Component,
FolderAddEditComponent,
FoldersComponent,
VaultFilterComponent,
HintComponent,
HomeComponent,
LoginViaAuthRequestComponentV1,
LoginComponentV1,
LoginDecryptionOptionsComponentV1,
NotificationsSettingsV1Component,
AppearanceComponent,
PasswordHistoryComponent,
RegisterComponent,
SetPasswordComponent,
VaultSettingsComponent,
ShareComponent,
SsoComponentV1,
SyncComponent,
TabsV2Component,
TwoFactorComponent,
TwoFactorOptionsComponent,
UpdateTempPasswordComponent,
UserVerificationComponent,
AccountSecurityComponentV1,
VaultTimeoutInputComponent,
ViewComponent,
ViewCustomFieldsComponent,
RemovePasswordComponent,
VaultSelectComponent,
Fido2V1Component,
AutofillV1Component,
EnvironmentSelectorComponent,

View File

@@ -1,7 +1,7 @@
import { inject } from "@angular/core";
import { CanDeactivateFn } from "@angular/router";
import { VaultV2Component } from "../popup/components/vault/vault-v2.component";
import { VaultV2Component } from "../popup/components/vault-v2/vault-v2.component";
import { VaultPopupItemsService } from "../popup/services/vault-popup-items.service";
import { VaultPopupListFiltersService } from "../popup/services/vault-popup-list-filters.service";

View File

@@ -1,102 +0,0 @@
<button
type="button"
class="row-btn"
(click)="view()"
appStopClick
appStopProp
appA11yTitle="{{ 'view' | i18n }}"
*ngIf="showView"
>
<i class="bwi bwi-lg bwi-list-alt" aria-hidden="true"></i>
</button>
<ng-container *ngIf="cipher.type === cipherType.Login">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'launch' | i18n }}"
(click)="launchCipher()"
*ngIf="!showView"
[ngClass]="{ disabled: !cipher.login.canLaunch }"
[attr.disabled]="!cipher.login.canLaunch ? '' : null"
>
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy(cipher, cipher.login.username, 'username', 'Username')"
[ngClass]="{ disabled: !cipher.login.username }"
[attr.disabled]="!cipher.login.username ? '' : null"
>
<i class="bwi bwi-lg bwi-user" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher, cipher.login.password, 'password', 'Password')"
[ngClass]="{ disabled: !cipher.login.password || !cipher.viewPassword }"
[attr.disabled]="!cipher.login.password ? '' : null"
>
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(cipher, cipher.login.totp, 'verificationCodeTotp', 'TOTP')"
[ngClass]="{ disabled: !displayTotpCopyButton(cipher) }"
[attr.disabled]="!displayTotpCopyButton(cipher) ? '' : null"
>
<i class="bwi bwi-lg bwi-clock" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.Card">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher, cipher.card.number, 'number', 'Card Number')"
[ngClass]="{ disabled: !cipher.card.number }"
[attr.disabled]="!cipher.card.number ? '' : null"
>
<i class="bwi bwi-lg bwi-hashtag" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
(click)="copy(cipher, cipher.card.code, 'securityCode', 'Security Code')"
[ngClass]="{ disabled: !cipher.card.code }"
[attr.disabled]="!cipher.card.code ? '' : null"
>
<i class="bwi bwi-lg bwi-key" aria-hidden="true"></i>
</button>
</ng-container>
<ng-container *ngIf="cipher.type === cipherType.SecureNote">
<button
type="button"
class="row-btn"
appStopClick
appStopProp
appA11yTitle="{{ 'copyNote' | i18n }}"
(click)="copy(cipher, cipher.notes, 'note', 'Note')"
[ngClass]="{ disabled: !cipher.notes }"
[attr.disabled]="!cipher.notes ? '' : null"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</ng-container>

View File

@@ -1,108 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { Subject, takeUntil } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordRepromptService } from "@bitwarden/vault";
@Component({
selector: "app-action-buttons",
templateUrl: "action-buttons.component.html",
})
export class ActionButtonsComponent implements OnInit, OnDestroy {
@Output() onView = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() showView = false;
cipherType = CipherType;
userHasPremiumAccess = false;
private componentIsDestroyed$ = new Subject<boolean>();
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private eventCollectionService: EventCollectionService,
private totpService: TotpServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private billingAccountProfileStateService: BillingAccountProfileStateService,
) {}
ngOnInit() {
this.billingAccountProfileStateService.hasPremiumFromAnySource$
.pipe(takeUntil(this.componentIsDestroyed$))
.subscribe((canAccessPremium: boolean) => {
this.userHasPremiumAccess = canAccessPremium;
});
}
ngOnDestroy() {
this.componentIsDestroyed$.next(true);
this.componentIsDestroyed$.complete();
}
launchCipher() {
this.launchEvent.emit(this.cipher);
}
async copy(cipher: CipherView, value: string, typeI18nKey: string, aType: string) {
if (
this.cipher.reprompt !== CipherRepromptType.None &&
this.passwordRepromptService.protectedFields().includes(aType) &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
}
if (value == null || (aType === "TOTP" && !this.displayTotpCopyButton(cipher))) {
return;
} else if (aType === "TOTP") {
value = await this.totpService.getCode(value);
}
if (!cipher.viewPassword) {
return;
}
this.platformUtilsService.copyToClipboard(value, { window: window });
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("valueCopied", this.i18nService.t(typeI18nKey)),
);
if (typeI18nKey === "password") {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.eventCollectionService.collect(EventType.Cipher_ClientCopiedPassword, cipher.id);
} else if (typeI18nKey === "verificationCodeTotp") {
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedHiddenField, cipher.id);
} else if (typeI18nKey === "securityCode") {
// 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.eventCollectionService.collect(EventType.Cipher_ClientCopiedCardCode, cipher.id);
}
}
displayTotpCopyButton(cipher: CipherView) {
return (
(cipher?.login?.hasTotp ?? false) && (cipher.organizationUseTotp || this.userHasPremiumAccess)
);
}
view() {
this.onView.emit(this.cipher);
}
}

View File

@@ -1,51 +0,0 @@
<div
role="group"
appA11yTitle="{{ cipher.name }}"
class="virtual-scroll-item"
[ngClass]="{ 'override-last': !last }"
>
<div class="box-content-row box-content-row-flex">
<button
type="button"
(click)="selectCipher(cipher)"
(dblclick)="launchCipher(cipher)"
appStopClick
title="{{ title }} - {{ cipher.name }}"
class="row-main"
>
<app-vault-icon [cipher]="cipher"></app-vault-icon>
<div class="row-main-content">
<span class="text">
<span class="truncate-box">
<span class="truncate">{{ cipher.name }}</span>
<ng-container *ngIf="cipher.organizationId">
<i
class="bwi bwi-collection text-muted"
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="cipher.hasAttachments">
<i
class="bwi bwi-paperclip text-muted"
title="{{ 'attachments' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "attachments" | i18n }}</span>
</ng-container>
</span>
</span>
<span class="detail">{{ cipher.subTitle }}</span>
</div>
</button>
<app-action-buttons
[cipher]="cipher"
[showView]="showView"
(onView)="viewCipher(cipher)"
(launchEvent)="launchCipher(cipher)"
class="action-buttons"
>
</app-action-buttons>
</div>
</div>

View File

@@ -1,31 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@Component({
selector: "app-cipher-row",
templateUrl: "cipher-row.component.html",
})
export class CipherRowComponent {
@Output() onSelected = new EventEmitter<CipherView>();
@Output() launchEvent = new EventEmitter<CipherView>();
@Output() onView = new EventEmitter<CipherView>();
@Input() cipher: CipherView;
@Input() last: boolean;
@Input() showView = false;
@Input() title: string;
selectCipher(c: CipherView) {
this.onSelected.emit(c);
}
launchCipher(c: CipherView) {
this.launchEvent.emit(c);
}
viewCipher(c: CipherView) {
this.onView.emit(c);
}
}

View File

@@ -171,7 +171,7 @@ describe("AddEditFolderDialogComponent", () => {
it("deletes the folder", async () => {
await component.deleteFolder();
expect(deleteFolder).toHaveBeenCalledWith(folderView.id);
expect(deleteFolder).toHaveBeenCalledWith(folderView.id, "");
expect(showToast).toHaveBeenCalledWith({
variant: "success",
title: null,

View File

@@ -13,7 +13,7 @@ import {
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -67,6 +67,7 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
name: ["", Validators.required],
});
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
private destroyRef = inject(DestroyRef);
constructor(
@@ -114,10 +115,10 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
this.folder.name = this.folderForm.controls.name.value;
try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(this.folder, userKey);
await this.folderApiService.save(folder);
await this.folderApiService.save(folder, activeUserId);
this.toastService.showToast({
variant: "success",
@@ -144,7 +145,8 @@ export class AddEditFolderDialogComponent implements AfterViewInit, OnInit {
}
try {
await this.folderApiService.delete(this.folder.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
await this.folderApiService.delete(this.folder.id, activeUserId);
this.toastService.showToast({
variant: "success",
title: null,

View File

@@ -34,7 +34,9 @@
"
class="{{ itemHeightClass }}"
>
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
<app-vault-icon [cipher]="cipher"></app-vault-icon>
</div>
<span data-testid="item-name">{{ cipher.name }}</span>
<i
*ngIf="cipher.organizationId"

View File

@@ -1,4 +1,5 @@
<bit-search
autocomplete="off"
[placeholder]="'search' | i18n"
[(ngModel)]="searchText"
(ngModelChange)="onSearchTextChanged()"

View File

@@ -18,12 +18,14 @@ 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 { VaultUiOnboardingService } from "../../services/vault-ui-onboarding.service";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
import {
NewItemDropdownV2Component,
NewItemInitialValues,
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
import { VaultHeaderV2Component } from "../vault-v2/vault-header/vault-header-v2.component";
} from "./new-item-dropdown/new-item-dropdown-v2.component";
import { VaultHeaderV2Component } from "./vault-header/vault-header-v2.component";
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from ".";
enum VaultState {
Empty,

View File

@@ -1,140 +0,0 @@
<div class="box">
<h2 class="box-header">
{{ "customFields" | i18n }}
</h2>
<div class="box-content">
<!-- Current custom fields -->
<div cdkDropList (cdkDropListDropped)="drop($event)" *ngIf="cipher.hasFields">
<div
role="group"
class="box-content-row box-content-row-multi box-draggable-row"
appBoxRow
cdkDrag
*ngFor="let f of cipher.fields; let i = index; trackBy: trackByFunction"
[ngClass]="{ 'box-content-row-checkbox': f.type === fieldType.Boolean }"
attr.aria-label="{{ f.name }}"
>
<button
type="button"
appStopClick
(click)="removeField(f)"
appA11yTitle="{{ 'remove' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<label for="fieldName{{ i }}" class="sr-only">{{ "name" | i18n }}</label>
<label for="fieldValue{{ i }}" class="sr-only">{{ "value" | i18n }}</label>
<div class="row-main">
<input
id="fieldName{{ i }}"
type="text"
name="Field.Name{{ i }}"
[(ngModel)]="f.name"
class="row-label"
placeholder="{{ 'name' | i18n }}"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
<!-- Text -->
<input
id="fieldValue{{ i }}"
type="text"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Text"
placeholder="{{ 'value' | i18n }}"
appInputVerbatim
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<!-- Hidden -->
<input
id="fieldValue{{ i }}"
type="{{ f.showValue ? 'text' : 'password' }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.value"
class="monospaced"
appInputVerbatim
*ngIf="f.type === fieldType.Hidden"
placeholder="{{ 'value' | i18n }}"
[disabled]="!cipher.viewPassword && !f.newField"
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<!-- Linked -->
<select
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
[(ngModel)]="f.linkedId"
*ngIf="f.type === fieldType.Linked && cipher.linkedFieldOptions != null"
attr.aria-describedby="fieldName{{ i }}"
>
<option *ngFor="let o of linkedFieldOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<!-- Boolean -->
<input
id="fieldValue{{ i }}"
name="Field.Value{{ i }}"
type="checkbox"
[(ngModel)]="f.value"
*ngIf="f.type === fieldType.Boolean"
appTrueFalseValue
trueValue="true"
falseValue="false"
attr.aria-describedby="fieldName{{ i }}"
[readonly]="!cipher.edit && editMode"
/>
<div
class="action-buttons"
*ngIf="f.type === fieldType.Hidden && (cipher.viewPassword || f.newField)"
>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleFieldValue(f)"
[attr.aria-pressed]="f.showValue"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !f.showValue, 'bwi-eye-slash': f.showValue }"
></i>
</button>
</div>
<div
class="drag-handle"
appA11yTitle="{{ 'dragToSort' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
cdkDragHandle
>
<i class="bwi bwi-hamburger" aria-hidden="true"></i>
</div>
</div>
</div>
<!-- Add new custom field -->
<div
class="box-content-row box-content-row-newmulti"
*ngIf="!(!cipher.edit && editMode)"
appBoxRow
>
<button type="button" appStopClick (click)="addField()">
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i>
{{ "newCustomField" | i18n }}
</button>
<label for="addFieldType" class="sr-only">{{ "type" | i18n }}</label>
<select id="addFieldType" name="AddFieldType" [(ngModel)]="addFieldType" class="field-type">
<option *ngFor="let o of addFieldTypeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option
*ngIf="cipher.linkedFieldOptions != null"
[ngValue]="addFieldLinkedTypeOption.value"
>
{{ addFieldLinkedTypeOption.name }}
</option>
</select>
</div>
</div>
</div>

View File

@@ -1,15 +0,0 @@
import { Component } from "@angular/core";
import { AddEditCustomFieldsComponent as BaseAddEditCustomFieldsComponent } from "@bitwarden/angular/vault/components/add-edit-custom-fields.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@Component({
selector: "app-vault-add-edit-custom-fields",
templateUrl: "add-edit-custom-fields.component.html",
})
export class AddEditCustomFieldsComponent extends BaseAddEditCustomFieldsComponent {
constructor(i18nService: I18nService, eventCollectionService: EventCollectionService) {
super(i18nService, eventCollectionService);
}
}

View File

@@ -1,826 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" *ngIf="cipher">
<app-callout type="info" *ngIf="allowOwnershipOptions() && !allowPersonal">
{{ "personalOwnershipPolicyInEffect" | i18n }}
</app-callout>
<div class="box">
<h2 class="box-header">
{{ "itemInformation" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input
id="name"
type="text"
name="Name"
[(ngModel)]="cipher.name"
[readonly]="!cipher.edit && editMode"
/>
</div>
<!-- Login -->
<div *ngIf="cipher.type === cipherType.Login">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginUsername">{{ "username" | i18n }}</label>
<input
id="loginUsername"
type="text"
name="Login.Username"
[(ngModel)]="cipher.login.username"
inputmode="email"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generateUsername' | i18n }}"
(click)="generateUsername()"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginPassword">{{ "password" | i18n }}</label>
<input
id="loginPassword"
class="monospaced"
type="{{ showPassword ? 'text' : 'password' }}"
name="Login.Password"
[(ngModel)]="cipher.login.password"
appInputVerbatim
[disabled]="!cipher.viewPassword"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
#checkPasswordBtn
class="row-btn btn"
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-fw bwi-lg bwi-check-circle"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-fw bwi-lg bwi-spinner bwi-spin"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
*ngIf="cipher.viewPassword && cipher.login.password"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'generatePassword' | i18n }}"
(click)="generatePassword()"
*ngIf="cipher.viewPassword && !(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-generate" aria-hidden="true"></i>
</button>
</div>
</div>
<!--Passkey-->
<div
class="box"
*ngIf="cipher.login.hasFido2Credentials && !cloneMode"
tabindex="0"
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
>
<div class="box-content">
<div class="box-content-row box-content-row-multi text-muted" appBoxRow>
<button
type="button"
appStopClick
(click)="removePasskey()"
appA11yTitle="{{ 'removePasskey' | i18n }}"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
{{ "dateCreated" | i18n }}
{{ cipher.login.fido2Credentials[0].creationDate | date: "short" }}
</div>
</div>
</div>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="loginTotp">{{ "authenticatorKeyTotp" | i18n }}</label>
<input
id="loginTotp"
type="{{ showTotpSeed ? 'text' : 'password' }}"
name="Login.Totp"
class="monospaced"
[(ngModel)]="cipher.login.totp"
appInputVerbatim
[disabled]="!cipher.viewPassword"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleTotpSeed()"
*ngIf="cipher.viewPassword && cipher.login.totp"
[attr.aria-pressed]="showTotpSeed"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showTotpSeed, 'bwi-eye-slash': showTotpSeed }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyTOTP' | i18n }}"
(click)="copy(cipher.login.totp, 'totp', 'TOTP')"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-fw bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'totpCapture' | i18n }}"
(click)="captureTOTPFromTab()"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-fw bwi-lg bwi-camera" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.type === cipherType.Card">
<div class="box-content-row" appBoxRow>
<label for="cardCardholderName">{{ "cardholderName" | i18n }}</label>
<input
id="cardCardholderName"
type="text"
name="Card.CardCardholderName"
[(ngModel)]="cipher.card.cardholderName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardNumber">{{ "number" | i18n }}</label>
<input
id="cardNumber"
class="monospaced"
type="{{ showCardNumber ? 'text' : 'password' }}"
name="Card.Number"
(input)="onCardNumberChange()"
[(ngModel)]="cipher.card.number"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardNumber()"
[attr.aria-pressed]="showCardNumber"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardBrand">{{ "brand" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardBrand">
<select id="cardBrand" name="Card.Brand" [(ngModel)]="cipher.card.brand">
<option *ngFor="let o of cardBrandOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</span>
<ng-template #readonlyCardBrand>
<input
id="cardBrand"
name="Card.Brand"
type="text"
[readonly]="true"
[value]="cipher.card.brand"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpMonth">{{ "expirationMonth" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyCardExpMonth">
<select id="cardExpMonth" name="Card.ExpMonth" [(ngModel)]="cipher.card.expMonth">
<option *ngFor="let o of cardExpMonthOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyCardExpMonth>
<input
id="cardExpMonth"
name="Card.ExpMonth"
type="text"
[readonly]="true"
[value]="getCardExpMonthDisplay()"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="cardExpYear">{{ "expirationYear" | i18n }}</label>
<input
id="cardExpYear"
type="text"
name="Card.ExpYear"
[(ngModel)]="cipher.card.expYear"
placeholder="{{ 'ex' | i18n }} {{ currentDate | date: 'yyyy' }}"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="cardCode">{{ "securityCode" | i18n }}</label>
<input
id="cardCode"
class="monospaced"
type="{{ showCardCode ? 'text' : 'password' }}"
name="Card.Code"
[(ngModel)]="cipher.card.code"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardCode()"
[attr.aria-pressed]="showCardCode"
>
<i
class="bwi bwi-fw bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
</div>
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.type === cipherType.Identity">
<div class="box-content-row" appBoxRow>
<label for="idTitle">{{ "title" | i18n }}</label>
<span *ngIf="!(!cipher.edit && editMode); else readonlyIdTitle">
<select id="idTitle" name="Identity.Title" [(ngModel)]="cipher.identity.title">
<option *ngFor="let o of identityTitleOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</span>
<ng-template #readonlyIdTitle>
<input
id="idTitle"
name="Identity.Title"
type="text"
[readonly]="true"
[value]="cipher.identity.title"
/>
</ng-template>
</div>
<div class="box-content-row" appBoxRow>
<label for="idFirstName">{{ "firstName" | i18n }}</label>
<input
id="idFirstName"
type="text"
name="Identity.FirstName"
[(ngModel)]="cipher.identity.firstName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idMiddleName">{{ "middleName" | i18n }}</label>
<input
id="idMiddleName"
type="text"
name="Identity.MiddleName"
[(ngModel)]="cipher.identity.middleName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLastName">{{ "lastName" | i18n }}</label>
<input
id="idLastName"
type="text"
name="Identity.LastName"
[(ngModel)]="cipher.identity.lastName"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idUsername">{{ "username" | i18n }}</label>
<input
id="idUsername"
type="text"
name="Identity.Username"
[(ngModel)]="cipher.identity.username"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCompany">{{ "company" | i18n }}</label>
<input
id="idCompany"
type="text"
name="Identity.Company"
[(ngModel)]="cipher.identity.company"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idSsn">{{ "ssn" | i18n }}</label>
<input
id="idSsn"
type="text"
name="Identity.SSN"
[(ngModel)]="cipher.identity.ssn"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPassportNumber">{{ "passportNumber" | i18n }}</label>
<input
id="idPassportNumber"
type="text"
name="Identity.PassportNumber"
[(ngModel)]="cipher.identity.passportNumber"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idLicenseNumber">{{ "licenseNumber" | i18n }}</label>
<input
id="idLicenseNumber"
type="text"
name="Identity.LicenseNumber"
[(ngModel)]="cipher.identity.licenseNumber"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idEmail">{{ "email" | i18n }}</label>
<input
id="idEmail"
type="text"
name="Identity.Email"
[(ngModel)]="cipher.identity.email"
appInputVerbatim
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPhone">{{ "phone" | i18n }}</label>
<input
id="idPhone"
type="text"
name="Identity.Phone"
[(ngModel)]="cipher.identity.phone"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress1">{{ "address1" | i18n }}</label>
<input
id="idAddress1"
type="text"
name="Identity.Address1"
[(ngModel)]="cipher.identity.address1"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress2">{{ "address2" | i18n }}</label>
<input
id="idAddress2"
type="text"
name="Identity.Address2"
[(ngModel)]="cipher.identity.address2"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idAddress3">{{ "address3" | i18n }}</label>
<input
id="idAddress3"
type="text"
name="Identity.Address3"
[(ngModel)]="cipher.identity.address3"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCity">{{ "cityTown" | i18n }}</label>
<input
id="idCity"
type="text"
name="Identity.City"
[(ngModel)]="cipher.identity.city"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idState">{{ "stateProvince" | i18n }}</label>
<input
id="idState"
type="text"
name="Identity.State"
[(ngModel)]="cipher.identity.state"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idPostalCode">{{ "zipPostalCode" | i18n }}</label>
<input
id="idPostalCode"
type="text"
name="Identity.PostalCode"
[(ngModel)]="cipher.identity.postalCode"
[readonly]="!cipher.edit && editMode"
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="idCountry">{{ "country" | i18n }}</label>
<input
id="idCountry"
type="text"
name="Identity.Country"
[(ngModel)]="cipher.identity.country"
[readonly]="!cipher.edit && editMode"
/>
</div>
</div>
<!-- SshKey -->
<div *ngIf="cipher.sshKey">
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
<span class="row-label"> {{ "sshPrivateKey" | i18n }}</span>
{{ cipher.sshKey.privateKey }}
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
<span class="row-label"> {{ "sshPublicKey" | i18n }}</span>
{{ cipher.sshKey.publicKey }}
</div>
<div
class="box-content-row"
*ngIf="cipher.sshKey.keyFingerprint"
style="overflow: hidden"
>
<span class="row-label"> {{ "sshKeyFingerprint" | i18n }}</span>
{{ cipher.sshKey.keyFingerprint }}
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.type === cipherType.Login">
<div class="box-content">
<ng-container *ngIf="cipher.login.hasUris">
<div
role="group"
class="box-content-row box-content-row-multi"
appBoxRow
*ngFor="let u of cipher.login.uris; let i = index; trackBy: trackByFunction"
attr.aria-label="{{ 'uriPosition' | i18n: i + 1 }}"
>
<button
type="button"
*ngIf="!(!cipher.edit && editMode)"
appStopClick
(click)="removeUri(u)"
appA11yTitle="{{ 'remove' | i18n }}"
>
<i class="bwi bwi-fw bwi-minus-circle bwi-lg" aria-hidden="true"></i>
</button>
<div class="row-main">
<label for="loginUri{{ i }}">{{ "uriPosition" | i18n: i + 1 }}</label>
<input
id="loginUri{{ i }}"
type="text"
name="Login.Uris[{{ i }}].Uri"
[(ngModel)]="u.uri"
[hidden]="$any(u).showUriOptionsInput === true"
placeholder="{{ 'ex' | i18n }} https://google.com"
inputmode="url"
[readonly]="!cipher.edit && editMode"
appInputVerbatim
/>
<label for="loginUriMatch{{ i }}" class="sr-only">
{{ "currentUri" | i18n }} {{ i + 1 }}
</label>
<select
*ngIf="currentUris && currentUris.length"
id="currentUris{{ i }}"
name="Login.Uris[{{ i }}].CurrentUris"
[(ngModel)]="u.uri"
[hidden]="!$any(u).showCurrentUris"
>
<option [ngValue]="null">-- {{ "select" | i18n }} --</option>
<option *ngFor="let u of currentUris" [ngValue]="u">{{ u }}</option>
</select>
<label for="loginUriMatch{{ i }}" class="sr-only">
{{ "matchDetection" | i18n }} {{ i + 1 }}
</label>
<select
id="loginUriMatch{{ i }}"
name="Login.Uris[{{ i }}].Match"
[(ngModel)]="u.match"
[hidden]="
$any(u).showOptions === false || ($any(u).showOptions == null && u.match == null)
"
(change)="loginUriMatchChanged(u)"
>
<option *ngFor="let o of uriMatchOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
<div class="action-buttons">
<button
type="button"
*ngIf="currentUris && currentUris.length"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleCurrentUris' | i18n }}"
(click)="toggleUriInput(u)"
[attr.aria-pressed]="$any(u).showCurrentUris === true"
>
<i aria-hidden="true" class="bwi bwi-fw bwi-lg bwi-list"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleOptions' | i18n }}"
(click)="toggleUriOptions(u)"
[attr.aria-pressed]="$any(u).showOptions === true"
[disabled]="!cipher.edit && editMode"
>
<i class="bwi bwi-fw bwi-lg bwi-cog" aria-hidden="true"></i>
</button>
</div>
</div>
</ng-container>
<button
type="button"
appStopClick
(click)="addUri()"
class="box-content-row box-content-row-newmulti single-line"
*ngIf="!(!cipher.edit && editMode)"
>
<i class="bwi bwi-plus-circle bwi-fw bwi-lg" aria-hidden="true"></i> {{ "newUri" | i18n }}
</button>
</div>
</div>
<div class="box" *ngIf="showAutoFillOnPageLoadOptions">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="autofillOnPageLoad">{{ "itemAutoFillOnPageLoad" | i18n }} </label>
<select
id="autofillOnPageLoad"
name="AutofillOnPageLoad"
[disabled]="reprompt"
[(ngModel)]="cipher.login.autofillOnPageLoad"
>
<option *ngFor="let o of autofillOnPageLoadOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
</div>
<div class="box-footer !tw-mb-0 !tw-pb-0" *ngIf="reprompt">
{{ "turnOffMasterPasswordPromptToEditField" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="folder">{{ "folder" | i18n }}</label>
<select id="folder" name="FolderId" [(ngModel)]="cipher.folderId">
<option *ngFor="let f of folders$ | async" [ngValue]="f.id">{{ f.name }}</option>
</select>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favorite">{{ "favorite" | i18n }}</label>
<input id="favorite" type="checkbox" name="Favorite" [(ngModel)]="cipher.favorite" />
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow *ngIf="canUseReprompt">
<label for="passwordPrompt">
{{ "passwordPrompt" | i18n }}
<a
target="_blank"
rel="noreferrer"
appA11yTitle="{{ 'learnMore' | i18n }}"
href="https://bitwarden.com/help/managing-items/#protect-individual-items"
>
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
</a>
</label>
<input
id="passwordPrompt"
type="checkbox"
name="PasswordPrompt"
[ngModel]="reprompt"
(change)="repromptChanged()"
[disabled]="!cipher.edit && editMode"
/>
</div>
<button
type="button"
class="box-content-row box-content-row-flex text-default single-line"
appStopClick
(click)="attachments()"
*ngIf="editMode && showAttachments && !cloneMode"
>
<div class="row-main">{{ "attachments" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg bwi-fw"
aria-hidden="true"
*ngIf="openAttachmentsInPopup"
></i>
<i
class="bwi bwi-angle-right row-sub-icon"
aria-hidden="true"
*ngIf="!openAttachmentsInPopup"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="editCollections()"
*ngIf="editMode && cipher.organizationId && !cloneMode"
>
<div class="row-main">{{ "collections" | i18n }}</div>
<i class="bwi bwi-angle-right row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box">
<h2 class="box-header">
<label for="notes">{{ "notes" | i18n }}</label>
</h2>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<textarea
id="notes"
name="Notes"
rows="6"
[(ngModel)]="cipher.notes"
[readonly]="!cipher.edit && editMode"
></textarea>
</div>
</div>
</div>
<app-vault-add-edit-custom-fields
*ngIf="!(!cipher.hasFields && !cipher.edit && editMode)"
[cipher]="cipher"
[thisCipherType]="cipher.type"
[editMode]="editMode"
>
</app-vault-add-edit-custom-fields>
<div class="box" *ngIf="allowOwnershipOptions()">
<h2 class="box-header">
{{ "ownership" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="organizationId">{{ "whoOwnsThisItem" | i18n }}</label>
<select
id="organizationId"
class="form-control"
name="OrganizationId"
[(ngModel)]="cipher.organizationId"
(change)="organizationChanged()"
>
<option *ngFor="let o of ownershipOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
</div>
<div class="box" *ngIf="(!editMode || cloneMode) && cipher.organizationId">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
<div class="box list" *ngIf="editMode && !cloneMode && (canDeleteCipher$ | async)">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
[appApiAction]="deletePromise"
#deleteBtn
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteItem" | i18n }}</span>
</div>
</button>
</div>
</div>
</main>
</form>

View File

@@ -1,417 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe, Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import qrcodeParser from "qrcode-parser";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { PopupCloseWarningService } from "../../../../popup/services/popup-close-warning.service";
import { Fido2UserVerificationService } from "../../../services/fido2-user-verification.service";
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
import { closeAddEditVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
@Component({
selector: "app-vault-add-edit",
templateUrl: "add-edit.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AddEditComponent extends BaseAddEditComponent implements OnInit {
currentUris: string[];
showAttachments = true;
openAttachmentsInPopup: boolean;
showAutoFillOnPageLoadOptions: boolean;
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
constructor(
cipherService: CipherService,
folderService: FolderService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
accountService: AccountService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
collectionService: CollectionService,
messagingService: MessagingService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
eventCollectionService: EventCollectionService,
policyService: PolicyService,
private popupCloseWarningService: PopupCloseWarningService,
organizationService: OrganizationService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
dialogService: DialogService,
datePipe: DatePipe,
configService: ConfigService,
private fido2UserVerificationService: Fido2UserVerificationService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
folderService,
i18nService,
platformUtilsService,
auditService,
accountService,
collectionService,
messagingService,
eventCollectionService,
policyService,
logService,
passwordRepromptService,
organizationService,
dialogService,
window,
datePipe,
configService,
cipherAuthorizationService,
);
}
async ngOnInit() {
await super.ngOnInit();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
}
if (params.folderId) {
this.folderId = params.folderId;
}
if (params.collectionId) {
this.collectionId = params.collectionId;
const collection = this.writeableCollections.find((c) => c.id === params.collectionId);
if (collection != null) {
this.collectionIds = [collection.id];
this.organizationId = collection.organizationId;
}
}
if (params.type) {
const type = parseInt(params.type, null);
this.type = type;
}
this.editMode = !params.cipherId;
if (params.cloneMode != null) {
this.cloneMode = params.cloneMode === "true";
}
if (params.selectedVault) {
this.organizationId = params.selectedVault;
}
await this.load();
if (!this.editMode || this.cloneMode) {
// Only allow setting username if there's no existing value
if (
params.username &&
(this.cipher.login.username == null || this.cipher.login.username === "")
) {
this.cipher.login.username = params.username;
}
if (params.name && (this.cipher.name == null || this.cipher.name === "")) {
this.cipher.name = params.name;
}
if (
params.uri &&
this.cipher.login.uris[0] &&
(this.cipher.login.uris[0].uri == null || this.cipher.login.uris[0].uri === "")
) {
this.cipher.login.uris[0].uri = params.uri;
}
}
this.openAttachmentsInPopup = BrowserPopupUtils.inPopup(window);
if (this.inAddEditPopoutWindow()) {
BrowserApi.messageListener("add-edit-popout", this.handleExtensionMessage.bind(this));
}
});
if (!this.editMode) {
const tabs = await BrowserApi.tabsQuery({ windowType: "normal" });
this.currentUris =
tabs == null
? null
: tabs.filter((tab) => tab.url != null && tab.url !== "").map((tab) => tab.url);
}
this.setFocus();
if (BrowserPopupUtils.inPopout(window)) {
this.popupCloseWarningService.enable();
}
}
async load() {
await super.load();
this.showAutoFillOnPageLoadOptions =
this.cipher.type === CipherType.Login &&
(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$));
}
async submit(): Promise<boolean> {
const fido2SessionData = await firstValueFrom(this.fido2PopoutSessionData$);
const { isFido2Session, sessionId, userVerification } = fido2SessionData;
const inFido2PopoutWindow = BrowserPopupUtils.inPopout(window) && isFido2Session;
// normalize card expiry year on save
if (this.cipher.type === this.cipherType.Card) {
this.cipher.card.expYear = normalizeExpiryYearFormat(this.cipher.card.expYear);
}
// TODO: Revert to use fido2 user verification service once user verification for passkeys is approved for production.
// PM-4577 - https://github.com/bitwarden/clients/pull/8746
if (
inFido2PopoutWindow &&
!(await this.handleFido2UserVerification(sessionId, userVerification))
) {
return false;
}
const success = await super.submit();
if (!success) {
return false;
}
if (BrowserPopupUtils.inPopout(window)) {
this.popupCloseWarningService.disable();
}
if (inFido2PopoutWindow) {
BrowserFido2UserInterfaceSession.confirmNewCredentialResponse(
sessionId,
this.cipher.id,
userVerification,
);
return true;
}
if (this.inAddEditPopoutWindow()) {
this.messagingService.send("addEditCipherSubmitted");
await closeAddEditVaultItemPopout(1000);
return true;
}
if (this.cloneMode) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"]);
} else {
this.location.back();
}
return true;
}
attachments() {
super.attachments();
if (this.openAttachmentsInPopup) {
const destinationUrl = this.router
.createUrlTree(["/attachments"], { queryParams: { cipherId: this.cipher.id } })
.toString();
const currentBaseUrl = window.location.href.replace(this.router.url, "");
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.openCurrentPagePopout(window, currentBaseUrl + destinationUrl);
} else {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/attachments"], { queryParams: { cipherId: this.cipher.id } });
}
}
editCollections() {
super.editCollections();
if (this.cipher.organizationId != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/collections"], { queryParams: { cipherId: this.cipher.id } });
}
}
async cancel() {
super.cancel();
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (BrowserPopupUtils.inPopout(window) && sessionData.isFido2Session) {
this.popupCloseWarningService.disable();
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (this.inAddEditPopoutWindow()) {
// 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
closeAddEditVaultItemPopout();
return;
}
this.location.back();
}
async generateUsername(): Promise<boolean> {
const confirmed = await super.generateUsername();
if (confirmed) {
await this.saveCipherState();
// 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(["generator"], { queryParams: { type: "username" } });
}
return confirmed;
}
async generatePassword(): Promise<boolean> {
const confirmed = await super.generatePassword();
if (confirmed) {
await this.saveCipherState();
// 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(["generator"], { queryParams: { type: "password" } });
}
return confirmed;
}
async delete(): Promise<boolean> {
const confirmed = await super.delete();
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/vault"]);
}
return confirmed;
}
toggleUriInput(uri: LoginUriView) {
const u = uri as any;
u.showCurrentUris = !u.showCurrentUris;
}
allowOwnershipOptions(): boolean {
return (
(!this.editMode || this.cloneMode) &&
this.ownershipOptions &&
(this.ownershipOptions.length > 1 || !this.allowPersonal)
);
}
private saveCipherState() {
return this.cipherService.setAddEditCipherInfo({
cipher: this.cipher,
collectionIds:
this.collections == null
? []
: this.collections.filter((c) => (c as any).checked).map((c) => c.id),
});
}
private setFocus() {
window.setTimeout(() => {
if (this.editMode) {
return;
}
if (this.cipher.name != null && this.cipher.name !== "") {
document.getElementById("loginUsername").focus();
} else {
document.getElementById("name").focus();
}
}, 200);
}
repromptChanged() {
super.repromptChanged();
if (!this.showAutoFillOnPageLoadOptions) {
return;
}
if (this.reprompt) {
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("passwordRepromptDisabledAutofillOnPageLoad"),
);
return;
}
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("autofillOnPageLoadSetToDefault"),
);
}
private inAddEditPopoutWindow() {
return BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.addEditVaultItem);
}
async captureTOTPFromTab() {
try {
const screenshot = await BrowserApi.captureVisibleTab();
const data = await qrcodeParser(screenshot);
const url = new URL(data.toString());
if (url.protocol == "otpauth:" && url.searchParams.has("secret")) {
this.cipher.login.totp = data.toString();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("totpCaptureSuccess"),
);
}
} catch (e) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("totpCaptureError"),
);
}
}
private handleExtensionMessage(message: { [key: string]: any; command: string }) {
if (message.command === "inlineAutofillMenuRefreshAddEditCipher") {
this.load().catch((error) => this.logService.error(error));
}
}
// TODO: Remove and use fido2 user verification service once user verification for passkeys is approved for production.
// Be sure to make the same changes to add-edit-v2.component.ts if applicable
private async handleFido2UserVerification(
sessionId: string,
userVerification: boolean,
): Promise<boolean> {
// We are bypassing user verification pending approval for production.
return true;
}
}

View File

@@ -1,72 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="close()" *ngIf="openedAttachmentsInPopup">
{{ "close" | i18n }}
</button>
<button type="button" (click)="back()" *ngIf="!openedAttachmentsInPopup">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "attachments" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box" *ngIf="cipher && cipher.hasAttachments">
<div class="box-content no-hover single-line">
<div class="box-content-row box-content-row-flex" *ngFor="let a of cipher.attachments">
<div class="row-main">
{{ a.fileName }}
</div>
<small class="row-sub-label">{{ a.sizeName }}</small>
<div class="action-buttons no-pad">
<button
type="button"
class="row-btn btn"
type="button"
appStopClick
appA11yTitle="{{ 'deleteAttachment' | i18n }}"
(click)="delete(a)"
#deleteBtn
[appApiAction]="deletePromises[a.id]"
[disabled]="$any(deleteBtn).loading"
>
<i
class="bwi bwi-trash bwi-lg bwi-fw"
[hidden]="$any(deleteBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
aria-hidden="true"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<h2 class="box-header">
{{ "newAttachment" | i18n }}
</h2>
<div class="box-content no-hover">
<div class="box-content-row">
<label for="file">{{ "file" | i18n }}</label>
<input type="file" id="file" name="file" aria-describedby="fileHelp" required />
</div>
</div>
<div id="fileHelp" class="box-footer">
{{ "maxFileSize" | i18n }}
</div>
</div>
</main>
</form>

View File

@@ -1,82 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-vault-attachments",
templateUrl: "attachments.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class AttachmentsComponent extends BaseAttachmentsComponent implements OnInit {
openedAttachmentsInPopup: boolean;
constructor(
cipherService: CipherService,
i18nService: I18nService,
keyService: KeyService,
encryptService: EncryptService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
private location: Location,
private route: ActivatedRoute,
stateService: StateService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
) {
super(
cipherService,
i18nService,
keyService,
encryptService,
platformUtilsService,
apiService,
window,
logService,
stateService,
fileDownloadService,
dialogService,
billingAccountProfileStateService,
accountService,
toastService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.init();
});
this.openedAttachmentsInPopup = history.length === 1;
}
back() {
this.location.back();
}
close() {
window.close();
}
}

View File

@@ -1,43 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<header>
<div class="left">
<button type="button" (click)="back()">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "collections" | i18n }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="$any(c).checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</main>
</form>

View File

@@ -1,61 +0,0 @@
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { CollectionsComponent as BaseCollectionsComponent } from "@bitwarden/angular/admin-console/components/collections.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { ToastService } from "@bitwarden/components";
@Component({
selector: "app-vault-collections",
templateUrl: "collections.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class CollectionsComponent extends BaseCollectionsComponent implements OnInit {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
cipherService: CipherService,
organizationService: OrganizationService,
private route: ActivatedRoute,
private location: Location,
logService: LogService,
accountService: AccountService,
toastService: ToastService,
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
organizationService,
logService,
accountService,
toastService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.onSavedCollections.subscribe(() => {
this.back();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.load();
});
}
back() {
this.location.back();
}
}

View File

@@ -1,95 +0,0 @@
<app-header>
<h1 class="sr-only">{{ "currentTab" | i18n }}</h1>
<div class="left">
<app-pop-out *ngIf="!inSidebar"></app-pop-out>
<button
type="button"
(click)="refresh()"
appA11yTitle="{{ 'refresh' | i18n }}"
*ngIf="inSidebar"
>
<i class="bwi bwi-refresh-tab bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
<div class="search center">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search$.next()"
autocomplete="off"
(keydown)="closeOnEsc($event)"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</app-header>
<main tabindex="-1">
<div class="no-items" *ngIf="!loaded">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="loaded">
<app-vault-select (onVaultSelectionChanged)="load()"></app-vault-select>
<div class="box list" *ngIf="loginCiphers">
<h2 class="box-header">
{{ "typeLogins" | i18n }}
<span class="flex-right">{{ loginCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let loginCipher of loginCiphers"
[cipher]="loginCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
>
</app-cipher-row>
<div class="box-content-row padded no-hover" *ngIf="!loginCiphers.length">
<p class="text-center">{{ "autoFillInfo" | i18n }}</p>
<button type="button" class="btn primary link block" (click)="addCipher()">
{{ "addLogin" | i18n }}
</button>
</div>
</div>
</div>
<div class="box list" *ngIf="cardCiphers && cardCiphers.length">
<h2 class="box-header">
{{ "cards" | i18n }}
<span class="flex-right">{{ cardCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let cardCipher of cardCiphers"
[cipher]="cardCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
></app-cipher-row>
</div>
</div>
<div class="box list" *ngIf="identityCiphers && identityCiphers.length">
<h2 class="box-header">
{{ "identities" | i18n }}
<span class="flex-right">{{ identityCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let identityCipher of identityCiphers"
[cipher]="identityCipher"
title="{{ 'autoFill' | i18n }}"
[showView]="true"
(onSelected)="fillCipher($event)"
(onView)="viewCipher($event)"
></app-cipher-row>
</div>
</div>
</ng-container>
</main>

View File

@@ -1,354 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Subject, firstValueFrom, from, Subscription } from "rxjs";
import { debounceTime, switchMap, takeUntil } from "rxjs/operators";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync";
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 { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { PasswordRepromptService } from "@bitwarden/vault";
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultFilterService } from "../../../services/vault-filter.service";
const BroadcasterSubscriptionId = "CurrentTabComponent";
@Component({
selector: "app-current-tab",
templateUrl: "current-tab.component.html",
})
export class CurrentTabComponent implements OnInit, OnDestroy {
pageDetails: any[] = [];
tab: chrome.tabs.Tab;
cardCiphers: CipherView[];
identityCiphers: CipherView[];
loginCiphers: CipherView[];
url: string;
hostname: string;
searchText: string;
inSidebar = false;
searchTypeSearch = false;
loaded = false;
isLoading = false;
showOrganizations = false;
showHowToAutofill = false;
autofillCalloutText: string;
protected search$ = new Subject<void>();
private destroy$ = new Subject<void>();
private collectPageDetailsSubscription: Subscription;
private totpCode: string;
private totpTimeout: number;
private loadedTimeout: number;
private searchTimeout: number;
constructor(
private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService,
private autofillService: AutofillService,
private i18nService: I18nService,
private router: Router,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private syncService: SyncService,
private searchService: SearchService,
private autofillSettingsService: AutofillSettingsServiceAbstraction,
private passwordRepromptService: PasswordRepromptService,
private organizationService: OrganizationService,
private vaultFilterService: VaultFilterService,
private vaultSettingsService: VaultSettingsService,
) {}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.inSidebar = BrowserPopupUtils.inSidebar(window);
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (this.isLoading) {
window.setTimeout(() => {
// 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.load();
}, 500);
}
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
if (!this.syncService.syncInProgress) {
await this.load();
await this.setCallout();
} else {
this.loadedTimeout = window.setTimeout(async () => {
if (!this.isLoading) {
await this.load();
await this.setCallout();
}
}, 5000);
}
this.search$
.pipe(
debounceTime(500),
switchMap(() => {
return from(this.searchVault());
}),
takeUntil(this.destroy$),
)
.subscribe();
const autofillOnPageLoadOrgPolicy = await firstValueFrom(
this.autofillSettingsService.activateAutofillOnPageLoadFromPolicy$,
);
const autofillOnPageLoadPolicyToastHasDisplayed = await firstValueFrom(
this.autofillSettingsService.autofillOnPageLoadPolicyToastHasDisplayed$,
);
// If the org "autofill on page load" policy is set, set the user setting to match it
// @TODO override user setting instead of overwriting
if (autofillOnPageLoadOrgPolicy === true) {
await this.autofillSettingsService.setAutofillOnPageLoad(true);
if (!autofillOnPageLoadPolicyToastHasDisplayed) {
this.platformUtilsService.showToast(
"info",
null,
this.i18nService.t("autofillPageLoadPolicyActivated"),
);
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(true);
}
}
// If the org policy is ever disabled after being enabled, reset the toast notification
if (!autofillOnPageLoadOrgPolicy && autofillOnPageLoadPolicyToastHasDisplayed) {
await this.autofillSettingsService.setAutofillOnPageLoadPolicyToastHasDisplayed(false);
}
}
ngOnDestroy() {
window.clearTimeout(this.loadedTimeout);
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
this.destroy$.next();
this.destroy$.complete();
}
async refresh() {
await this.load();
}
addCipher() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
name: this.hostname,
uri: this.url,
selectedVault: this.vaultFilterService.getVaultFilter().selectedOrganizationId,
},
});
}
viewCipher(cipher: CipherView) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
}
async fillCipher(cipher: CipherView, closePopupDelay?: number) {
if (
cipher.reprompt !== CipherRepromptType.None &&
!(await this.passwordRepromptService.showPasswordPrompt())
) {
return;
}
this.totpCode = null;
if (this.totpTimeout != null) {
window.clearTimeout(this.totpTimeout);
}
if (this.pageDetails == null || this.pageDetails.length === 0) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
return;
}
try {
this.totpCode = await this.autofillService.doAutoFill({
tab: this.tab,
cipher: cipher,
pageDetails: this.pageDetails,
doc: window.document,
fillNewPassword: true,
allowTotpAutofill: true,
});
if (this.totpCode != null) {
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
}
if (BrowserPopupUtils.inPopup(window)) {
if (!closePopupDelay) {
if (this.platformUtilsService.isFirefox() || this.platformUtilsService.isSafari()) {
BrowserApi.closePopup(window);
} else {
// Slight delay to fix bug in Chromium browsers where popup closes without copying totp to clipboard
setTimeout(() => BrowserApi.closePopup(window), 50);
}
} else {
setTimeout(() => BrowserApi.closePopup(window), closePopupDelay);
}
}
} catch {
this.ngZone.run(() => {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
this.changeDetectorRef.detectChanges();
});
}
}
async searchVault() {
if (!(await this.searchService.isSearchable(this.searchText))) {
return;
}
await this.router.navigate(["/tabs/vault"], { queryParams: { searchText: this.searchText } });
}
closeOnEsc(e: KeyboardEvent) {
// If input not empty, use browser default behavior of clearing input instead
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
BrowserApi.closePopup(window);
}
}
protected async load() {
this.isLoading = false;
this.tab = await BrowserApi.getTabFromCurrentWindow();
if (this.tab != null) {
this.url = this.tab.url;
} else {
this.loginCiphers = [];
this.isLoading = this.loaded = true;
return;
}
this.pageDetails = [];
this.collectPageDetailsSubscription?.unsubscribe();
this.collectPageDetailsSubscription = this.autofillService
.collectPageDetailsFromTab$(this.tab)
.pipe(takeUntil(this.destroy$))
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
this.hostname = Utils.getHostname(this.url);
const otherTypes: CipherType[] = [];
const dontShowCards = !(await firstValueFrom(this.vaultSettingsService.showCardsCurrentTab$));
const dontShowIdentities = !(await firstValueFrom(
this.vaultSettingsService.showIdentitiesCurrentTab$,
));
this.showOrganizations = await this.organizationService.hasOrganizations();
if (!dontShowCards) {
otherTypes.push(CipherType.Card);
}
if (!dontShowIdentities) {
otherTypes.push(CipherType.Identity);
}
const ciphers = await this.cipherService.getAllDecryptedForUrl(
this.url,
otherTypes.length > 0 ? otherTypes : null,
);
this.loginCiphers = [];
this.cardCiphers = [];
this.identityCiphers = [];
ciphers.forEach((c) => {
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
switch (c.type) {
case CipherType.Login:
this.loginCiphers.push(c);
break;
case CipherType.Card:
this.cardCiphers.push(c);
break;
case CipherType.Identity:
this.identityCiphers.push(c);
break;
default:
break;
}
}
});
if (this.loginCiphers.length) {
this.loginCiphers = this.loginCiphers.sort((a, b) =>
this.cipherService.sortCiphersByLastUsedThenName(a, b),
);
}
this.isLoading = this.loaded = true;
}
async goToSettings() {
// 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(["autofill"]);
}
async dismissCallout() {
await this.autofillSettingsService.setAutofillOnPageLoadCalloutIsDismissed(true);
this.showHowToAutofill = false;
}
private async setCallout() {
const inlineMenuVisibilityIsOff =
(await firstValueFrom(this.autofillSettingsService.inlineMenuVisibility$)) ===
AutofillOverlayVisibility.Off;
this.showHowToAutofill =
this.loginCiphers.length > 0 &&
inlineMenuVisibilityIsOff &&
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$)) &&
!(await firstValueFrom(this.autofillSettingsService.autofillOnPageLoadCalloutIsDismissed$));
if (this.showHowToAutofill) {
const autofillCommand = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillCalloutText(autofillCommand);
}
}
private setAutofillCalloutText(command: string) {
if (command) {
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithCommand", command);
} else {
this.autofillCalloutText = this.i18nService.t("autofillSelectInfoWithoutCommand");
}
}
}

View File

@@ -1,40 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "passwordHistory" | i18n }}</span>
</h1>
<div class="right"></div>
</header>
<main tabindex="-1">
<div class="box list full-list" *ngIf="history && history.length">
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
<div class="row-main">
<div class="row-main-content">
<span
class="text monospaced no-ellipsis"
[innerHTML]="h.password | colorPassword"
></span>
<span class="detail">{{ h.lastUsedDate | date: "medium" }}</span>
</div>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div class="no-items" *ngIf="!history || !history.length">
<p>{{ "noPasswordsInList" | i18n }}</p>
</div>
</main>

View File

@@ -1,44 +0,0 @@
import { Location } from "@angular/common";
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { first } from "rxjs/operators";
import { PasswordHistoryComponent as BasePasswordHistoryComponent } from "@bitwarden/angular/vault/components/password-history.component";
import { AccountService } from "@bitwarden/common/auth/abstractions/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";
@Component({
selector: "app-password-history",
templateUrl: "password-history.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class PasswordHistoryComponent extends BasePasswordHistoryComponent implements OnInit {
constructor(
cipherService: CipherService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
accountService: AccountService,
private location: Location,
private route: ActivatedRoute,
) {
super(cipherService, platformUtilsService, i18nService, accountService, window);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
} else {
this.close();
}
await this.init();
});
}
close() {
this.location.back();
}
}

View File

@@ -1,77 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<ng-container *ngIf="organizations$ | async as organizations">
<header>
<div class="left">
<button type="button" (click)="cancel()">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "moveToOrganization" | i18n }}</span>
</h1>
<div class="right">
<button
type="submit"
[disabled]="form.loading || !canSave"
*ngIf="organizations && organizations.length"
>
<span [hidden]="form.loading">{{ "move" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!form.loading"
aria-hidden="true"
></i>
</button>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content" *ngIf="!organizations || !organizations.length">
<div class="box-content-row padded no-hover">
{{ "noOrganizationsList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="organizations && organizations.length">
<div class="box-content-row" appBoxRow>
<label for="organization">{{ "organization" | i18n }}</label>
<select
id="organization"
name="OrganizationId"
aria-describedby="organizationHelp"
[(ngModel)]="organizationId"
(change)="filterCollections()"
>
<option *ngFor="let o of organizations" [ngValue]="o.id">{{ o.name }}</option>
</select>
</div>
</div>
<div id="organizationHelp" class="box-footer">
{{ "moveToOrgDesc" | i18n }}
</div>
</div>
<div class="box" *ngIf="organizations && organizations.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content" *ngIf="!collections || !collections.length">
<div class="box-content-row padded no-hover">
{{ "noCollectionsInList" | i18n }}
</div>
</div>
<div class="box-content" *ngIf="collections && collections.length">
<div
class="box-content-row box-content-row-checkbox"
*ngFor="let c of collections; let i = index"
appBoxRow
>
<label for="collection_{{ i }}">{{ c.name }}</label>
<input
id="collection_{{ i }}"
type="checkbox"
[(ngModel)]="c.checked"
name="Collection[{{ i }}].Checked"
/>
</div>
</div>
</div>
</main>
</ng-container>
</form>

View File

@@ -1,72 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService } from "@bitwarden/admin-console/common";
import { ShareComponent as BaseShareComponent } from "@bitwarden/angular/components/share.component";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@Component({
selector: "app-vault-share",
templateUrl: "share.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class ShareComponent extends BaseShareComponent implements OnInit {
constructor(
collectionService: CollectionService,
platformUtilsService: PlatformUtilsService,
i18nService: I18nService,
logService: LogService,
cipherService: CipherService,
private route: ActivatedRoute,
private router: Router,
organizationService: OrganizationService,
accountService: AccountService,
) {
super(
collectionService,
platformUtilsService,
i18nService,
cipherService,
logService,
organizationService,
accountService,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.onSharedCipher.subscribe(() => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["view-cipher", { cipherId: this.cipherId }]);
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.cipherId = params.cipherId;
await this.load();
});
}
async submit(): Promise<boolean> {
const success = await super.submit();
if (success) {
this.cancel();
}
return success;
}
cancel() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
replaceUrl: true,
queryParams: { cipherId: this.cipher.id },
});
}
}

View File

@@ -1,238 +0,0 @@
<app-header>
<div class="left">
<app-pop-out></app-pop-out>
</div>
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
<div class="search center">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ 'searchVault' | i18n }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
(keydown)="closeOnEsc($event)"
/>
<i class="bwi bwi-search"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</app-header>
<main tabindex="-1" cdk-scrollable>
<app-vault-select
(onVaultSelectionChanged)="vaultFilterChanged()"
class="select-index-top"
></app-vault-select>
<div class="no-items" *ngIf="(!ciphers || !ciphers.length) && !showSearching()">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button type="button" (click)="addCipher()" class="btn block primary link">
{{ "addItem" | i18n }}
</button>
</ng-container>
</div>
<ng-container *ngIf="ciphers && ciphers.length && !showSearching()">
<div class="box list" *ngIf="favoriteCiphers">
<h2 class="box-header">
{{ "favorites" | i18n }}
<span class="flex-right">{{ favoriteCiphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let favoriteCipher of favoriteCiphers"
[cipher]="favoriteCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
<div class="box list">
<h2 class="box-header">
{{ "types" | i18n }}
<span class="flex-right">4</span>
</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Login)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-globe"></i></div>
<span class="text">{{ "typeLogin" | i18n }}</span>
</div>
<span class="row-sub-label">
{{ typeCounts.get(cipherType.Login) || 0 }}
</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Card)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-credit-card"></i></div>
<span class="text">{{ "typeCard" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.Card) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.Identity)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-id-card"></i></div>
<span class="text">{{ "typeIdentity" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.Identity) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="selectType(cipherType.SecureNote)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-sticky-note"></i></div>
<span class="text">{{ "typeSecureNote" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.SecureNote) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
<button
type="button"
class="box-content-row"
appStopClick
*ngIf="isSshKeysEnabled"
(click)="selectType(cipherType.SshKey)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-key"></i></div>
<span class="text">{{ "typeSshKey" | i18n }}</span>
</div>
<span class="row-sub-label">{{ typeCounts.get(cipherType.SshKey) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="nestedFolders?.length">
<h2 class="box-header">
{{ "folders" | i18n }}
<span class="flex-right">{{ folderCount }}</span>
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let f of nestedFolders"
class="box-content-row"
appStopClick
(click)="selectFolder(f.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-folder"></i>
</div>
<span class="text">{{ f.node.name }}</span>
</div>
<span class="row-sub-label">{{ folderCounts.get(f.node.id) || 0 }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="showCollections && nestedCollections && nestedCollections.length">
<h2 class="box-header">
{{ "collections" | i18n }}
<span class="flex-right">{{ nestedCollections.length }}</span>
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let nestedCollection of nestedCollections"
class="box-content-row"
appStopClick
(click)="selectCollection(nestedCollection.node)"
>
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-collection"></i></div>
<span class="text">{{ nestedCollection.node.name }}</span>
</div>
<span class="row-sub-label">{{
collectionCounts.get(nestedCollection.node.id) || 0
}}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="showNoFolderCiphers">
<h2 class="box-header">
{{ "noneFolder" | i18n }}
<div class="flex-right">{{ noFolderCiphers.length }}</div>
</h2>
<div class="box-content">
<app-cipher-row
*ngFor="let noFolderCipher of noFolderCiphers"
[cipher]="noFolderCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
<div class="box list" *ngIf="deletedCount">
<h2 class="box-header">
{{ "trash" | i18n }}
<span class="flex-right">{{ deletedCount }}</span>
</h2>
<div class="box-content single-line">
<button type="button" class="box-content-row" appStopClick (click)="selectTrash()">
<div class="row-main">
<div class="icon"><i class="bwi bwi-fw bwi-lg bwi-trash"></i></div>
<span class="text">{{ "trash" | i18n }}</span>
</div>
<span class="row-sub-label">{{ deletedCount }}</span>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon"></i></span>
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="showSearching()">
<div class="no-items" *ngIf="!ciphers || !ciphers.length">
<p>{{ "noItemsInList" | i18n }}</p>
</div>
<cdk-virtual-scroll-viewport
itemSize="55"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers && ciphers.length > 0"
>
<div class="box list full-list">
<div class="box-content">
<app-cipher-row
*cdkVirtualFor="let searchedCipher of ciphers"
[cipher]="searchedCipher"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
>
</app-cipher-row>
</div>
</div>
</cdk-virtual-scroll-viewport>
</ng-container>
</main>

View File

@@ -1,482 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { BehaviorSubject, Subject, firstValueFrom, from } from "rxjs";
import { first, switchMap, takeUntil } from "rxjs/operators";
import { CollectionView } from "@bitwarden/admin-console/common";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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 { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { BrowserGroupingsComponentState } from "../../../../models/browserGroupingsComponentState";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
import { VaultFilterService } from "../../../services/vault-filter.service";
const ComponentId = "VaultComponent";
@Component({
selector: "app-vault-filter",
templateUrl: "vault-filter.component.html",
})
export class VaultFilterComponent implements OnInit, OnDestroy {
get showNoFolderCiphers(): boolean {
return (
this.noFolderCiphers != null &&
this.noFolderCiphers.length < this.noFolderListSize &&
this.collections.length === 0
);
}
get folderCount(): number {
return this.nestedFolders.length - (this.showNoFolderCiphers ? 0 : 1);
}
folders: FolderView[];
nestedFolders: TreeNode<FolderView>[];
collections: CollectionView[];
nestedCollections: TreeNode<CollectionView>[];
loaded = false;
cipherType = CipherType;
ciphers: CipherView[];
favoriteCiphers: CipherView[];
noFolderCiphers: CipherView[];
folderCounts = new Map<string, number>();
collectionCounts = new Map<string, number>();
typeCounts = new Map<CipherType, number>();
state: BrowserGroupingsComponentState;
showLeftHeader = true;
searchPending = false;
searchTypeSearch = false;
deletedCount = 0;
vaultFilter: VaultFilter;
selectedOrganization: string = null;
showCollections = true;
isSshKeysEnabled = false;
private loadedTimeout: number;
private selectedTimeout: number;
private preventSelected = false;
private noFolderListSize = 100;
private searchTimeout: any = null;
private hasSearched = false;
private hasLoadedAllCiphers = false;
private allCiphers: CipherView[] = null;
private destroy$ = new Subject<void>();
private _searchText$ = new BehaviorSubject<string>("");
private isSearchable: boolean = false;
get searchText() {
return this._searchText$.value;
}
set searchText(value: string) {
this._searchText$.next(value);
}
constructor(
private i18nService: I18nService,
private cipherService: CipherService,
private router: Router,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private route: ActivatedRoute,
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private searchService: SearchService,
private location: Location,
private vaultFilterService: VaultFilterService,
private vaultBrowserStateService: VaultBrowserStateService,
private configService: ConfigService,
) {
this.noFolderListSize = 100;
}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showLeftHeader = !(
BrowserPopupUtils.inSidebar(window) && this.platformUtilsService.isFirefox()
);
await this.vaultBrowserStateService.setBrowserVaultItemsComponentState(null);
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
window.setTimeout(() => {
// 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.load();
}, 500);
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
const restoredScopeState = await this.restoreState();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
if (this.state?.searchText) {
this.searchText = this.state.searchText;
} else if (params.searchText) {
this.searchText = params.searchText;
this.location.replaceState("vault");
}
if (!this.syncService.syncInProgress) {
// 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.load();
} else {
this.loadedTimeout = window.setTimeout(() => {
if (!this.loaded) {
// 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.load();
}
}, 5000);
}
if (!this.syncService.syncInProgress || restoredScopeState) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserPopupUtils.setContentScrollY(window, this.state?.scrollY);
}
});
this._searchText$
.pipe(
switchMap((searchText) => from(this.searchService.isSearchable(searchText))),
takeUntil(this.destroy$),
)
.subscribe((isSearchable) => {
this.isSearchable = isSearchable;
});
this.isSshKeysEnabled = await this.configService.getFeatureFlag(FeatureFlag.SSHKeyVaultItem);
}
ngOnDestroy() {
if (this.loadedTimeout != null) {
window.clearTimeout(this.loadedTimeout);
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
// 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.saveState();
this.broadcasterService.unsubscribe(ComponentId);
this.destroy$.next();
this.destroy$.complete();
}
async load() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
this.updateSelectedOrg();
await this.loadCollectionsAndFolders();
await this.loadCiphers();
if (this.showNoFolderCiphers && this.nestedFolders.length > 0) {
// Remove "No Folder" from folder listing
this.nestedFolders = this.nestedFolders.slice(0, this.nestedFolders.length - 1);
}
this.loaded = true;
}
async loadCiphers() {
this.allCiphers = await this.cipherService.getAllDecrypted();
if (!this.hasLoadedAllCiphers) {
this.hasLoadedAllCiphers = !(await this.searchService.isSearchable(this.searchText));
}
await this.search(null);
this.getCounts();
}
async loadCollections() {
const allCollections = await this.vaultFilterService.buildCollections(
this.selectedOrganization,
);
this.collections = allCollections.fullList;
this.nestedCollections = allCollections.nestedList;
}
async loadFolders() {
const allFolders = await firstValueFrom(
this.vaultFilterService.buildNestedFolders(this.selectedOrganization),
);
this.folders = allFolders.fullList;
this.nestedFolders = allFolders.nestedList;
}
async search(timeout: number = null) {
this.searchPending = false;
if (this.searchTimeout != null) {
clearTimeout(this.searchTimeout);
}
const filterDeleted = (c: CipherView) => !c.isDeleted;
if (timeout == null) {
this.hasSearched = this.isSearchable;
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
filterDeleted,
this.allCiphers,
);
this.ciphers = this.ciphers.filter(
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
);
return;
}
this.searchPending = true;
this.searchTimeout = setTimeout(async () => {
this.hasSearched = this.isSearchable;
if (!this.hasLoadedAllCiphers && !this.hasSearched) {
await this.loadCiphers();
} else {
this.ciphers = await this.searchService.searchCiphers(
this.searchText,
filterDeleted,
this.allCiphers,
);
}
this.ciphers = this.ciphers.filter(
(c) => !this.vaultFilterService.filterCipherForSelectedVault(c),
);
this.searchPending = false;
}, timeout);
}
async selectType(type: CipherType) {
// 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(["/ciphers"], { queryParams: { type: type } });
}
async selectFolder(folder: FolderView) {
// 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(["/ciphers"], { queryParams: { folderId: folder.id || "none" } });
}
async selectCollection(collection: CollectionView) {
// 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(["/ciphers"], { queryParams: { collectionId: collection.id } });
}
async selectTrash() {
// 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(["/ciphers"], { queryParams: { deleted: true } });
}
async selectCipher(cipher: CipherView) {
this.selectedTimeout = window.setTimeout(() => {
if (!this.preventSelected) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], { queryParams: { cipherId: cipher.id } });
}
this.preventSelected = false;
}, 200);
}
async launchCipher(cipher: CipherView) {
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
return;
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
this.preventSelected = true;
await this.cipherService.updateLastLaunchedDate(cipher.id);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab(cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
async addCipher() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: { selectedVault: this.vaultFilter.selectedOrganizationId },
});
}
async vaultFilterChanged() {
if (this.showSearching) {
await this.search();
}
this.updateSelectedOrg();
await this.loadCollectionsAndFolders();
this.getCounts();
}
updateSelectedOrg() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
if (this.vaultFilter.selectedOrganizationId != null) {
this.selectedOrganization = this.vaultFilter.selectedOrganizationId;
} else {
this.selectedOrganization = null;
}
}
getCounts() {
let favoriteCiphers: CipherView[] = null;
let noFolderCiphers: CipherView[] = null;
const folderCounts = new Map<string, number>();
const collectionCounts = new Map<string, number>();
const typeCounts = new Map<CipherType, number>();
this.deletedCount = this.allCiphers.filter(
(c) => c.isDeleted && !this.vaultFilterService.filterCipherForSelectedVault(c),
).length;
this.ciphers?.forEach((c) => {
if (!this.vaultFilterService.filterCipherForSelectedVault(c)) {
if (c.isDeleted) {
return;
}
if (c.favorite) {
if (favoriteCiphers == null) {
favoriteCiphers = [];
}
favoriteCiphers.push(c);
}
if (c.folderId == null) {
if (noFolderCiphers == null) {
noFolderCiphers = [];
}
noFolderCiphers.push(c);
}
if (typeCounts.has(c.type)) {
typeCounts.set(c.type, typeCounts.get(c.type) + 1);
} else {
typeCounts.set(c.type, 1);
}
if (folderCounts.has(c.folderId)) {
folderCounts.set(c.folderId, folderCounts.get(c.folderId) + 1);
} else {
folderCounts.set(c.folderId, 1);
}
if (c.collectionIds != null) {
c.collectionIds.forEach((colId) => {
if (collectionCounts.has(colId)) {
collectionCounts.set(colId, collectionCounts.get(colId) + 1);
} else {
collectionCounts.set(colId, 1);
}
});
}
}
});
this.favoriteCiphers = favoriteCiphers;
this.noFolderCiphers = noFolderCiphers;
this.typeCounts = typeCounts;
this.folderCounts = folderCounts;
this.collectionCounts = collectionCounts;
}
showSearching() {
return this.hasSearched || (!this.searchPending && this.isSearchable);
}
closeOnEsc(e: KeyboardEvent) {
// If input not empty, use browser default behavior of clearing input instead
if (e.key === "Escape" && (this.searchText == null || this.searchText === "")) {
BrowserApi.closePopup(window);
}
}
private async loadCollectionsAndFolders() {
this.showCollections = !this.vaultFilter.myVaultOnly;
await this.loadFolders();
await this.loadCollections();
}
private async saveState() {
this.state = Object.assign(new BrowserGroupingsComponentState(), {
scrollY: BrowserPopupUtils.getContentScrollY(window),
searchText: this.searchText,
favoriteCiphers: this.favoriteCiphers,
noFolderCiphers: this.noFolderCiphers,
ciphers: this.ciphers,
collectionCounts: this.collectionCounts,
folderCounts: this.folderCounts,
typeCounts: this.typeCounts,
folders: this.folders,
collections: this.collections,
deletedCount: this.deletedCount,
});
await this.vaultBrowserStateService.setBrowserGroupingsComponentState(this.state);
}
private async restoreState(): Promise<boolean> {
this.state = await this.vaultBrowserStateService.getBrowserGroupingsComponentState();
if (this.state == null) {
return false;
}
if (this.state.favoriteCiphers != null) {
this.favoriteCiphers = this.state.favoriteCiphers;
}
if (this.state.noFolderCiphers != null) {
this.noFolderCiphers = this.state.noFolderCiphers;
}
if (this.state.ciphers != null) {
this.ciphers = this.state.ciphers;
}
if (this.state.collectionCounts != null) {
this.collectionCounts = this.state.collectionCounts;
}
if (this.state.folderCounts != null) {
this.folderCounts = this.state.folderCounts;
}
if (this.state.typeCounts != null) {
this.typeCounts = this.state.typeCounts;
}
if (this.state.folders != null) {
this.folders = this.state.folders;
}
if (this.state.collections != null) {
this.collections = this.state.collections;
}
if (this.state.deletedCount != null) {
this.deletedCount = this.state.deletedCount;
}
return true;
}
}

View File

@@ -1,123 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="back()">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="sr-only">{{ "myVault" | i18n }}</h1>
<div class="search">
<input
type="{{ searchTypeSearch ? 'search' : 'text' }}"
placeholder="{{ searchPlaceholder || ('searchVault' | i18n) }}"
id="search"
[(ngModel)]="searchText"
(input)="search(200)"
autocomplete="off"
appAutofocus
/>
<i class="bwi bwi-search" aria-hidden="true"></i>
</div>
<div class="right">
<button type="button" (click)="addCipher()" appA11yTitle="{{ 'addItem' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" [ngClass]="{ 'stacked-boxes': showGroupings() }">
<ng-container *ngIf="showGroupings()">
<app-vault-select
*ngIf="showVaultFilter"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="box list" *ngIf="nestedFolders && nestedFolders.length">
<h2 class="box-header">
{{ "folders" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let f of nestedFolders"
class="box-content-row"
appStopClick
(click)="selectFolder(f.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-folder" aria-hidden="true"></i>
</div>
<span class="text">{{ f.node.name }}</span>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
</div>
</div>
<div class="box list" *ngIf="nestedCollections && nestedCollections.length">
<h2 class="box-header">
{{ "collections" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
*ngFor="let c of nestedCollections"
class="box-content-row"
appStopClick
(click)="selectCollection(c.node)"
>
<div class="row-main">
<div class="icon">
<i class="bwi bwi-fw bwi-lg bwi-collection" aria-hidden="true"></i>
</div>
<span class="text">{{ c.node.name }}</span>
</div>
<span><i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i></span>
</button>
</div>
</div>
</ng-container>
<ng-container *ngIf="ciphers">
<div *ngIf="!ciphers.length">
<app-vault-select
*ngIf="showVaultFilter && !showGroupings()"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="no-items" *ngIf="!nestedFolders?.length && !nestedCollections?.length">
<i class="bwi bwi-spinner bwi-spin bwi-3x" *ngIf="!loaded" aria-hidden="true"></i>
<ng-container *ngIf="loaded">
<img class="no-items-image" aria-hidden="true" />
<p>{{ "noItemsInList" | i18n }}</p>
<button type="button" (click)="addCipher()" class="btn block primary link">
{{ "addItem" | i18n }}
</button>
</ng-container>
</div>
</div>
<cdk-virtual-scroll-viewport
itemSize="55"
minBufferPx="400"
maxBufferPx="600"
*ngIf="ciphers.length"
#virtualScrollViewport
><app-vault-select
*ngIf="showVaultFilter && !showGroupings()"
(onVaultSelectionChanged)="changeVaultSelection()"
></app-vault-select>
<div class="box list only-list">
<h2 class="box-header">
{{ groupingTitle }}
<span class="flex-right">{{ isSearching() ? ciphers.length : ciphers.length }}</span>
</h2>
<div class="box-content">
<app-cipher-row
*cdkVirtualFor="let c of ciphers; let last = last"
[cipher]="c"
[last]="last"
title="{{ 'viewItem' | i18n }}"
(onSelected)="selectCipher($event)"
(launchEvent)="launchCipher($event)"
></app-cipher-row>
</div>
</div>
</cdk-virtual-scroll-viewport>
</ng-container>
</main>

View File

@@ -1,316 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { BrowserComponentState } from "../../../../models/browserComponentState";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { VaultBrowserStateService } from "../../../services/vault-browser-state.service";
import { VaultFilterService } from "../../../services/vault-filter.service";
const ComponentId = "VaultItemsComponent";
@Component({
selector: "app-vault-items",
templateUrl: "vault-items.component.html",
})
export class VaultItemsComponent extends BaseVaultItemsComponent implements OnInit, OnDestroy {
groupingTitle: string;
state: BrowserComponentState;
folderId: string = null;
collectionId: string = null;
type: CipherType = null;
nestedFolders: TreeNode<FolderView>[];
nestedCollections: TreeNode<CollectionView>[];
searchTypeSearch = false;
showOrganizations = false;
vaultFilter: VaultFilter;
deleted = true;
noneFolder = false;
showVaultFilter = false;
private selectedTimeout: number;
private preventSelected = false;
private applySavedState = true;
private scrollingContainer = "cdk-virtual-scroll-viewport";
constructor(
searchService: SearchService,
private organizationService: OrganizationService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
private ngZone: NgZone,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private stateService: VaultBrowserStateService,
private i18nService: I18nService,
private collectionService: CollectionService,
private platformUtilsService: PlatformUtilsService,
cipherService: CipherService,
private vaultFilterService: VaultFilterService,
) {
super(searchService, cipherService);
this.applySavedState =
(window as any).previousPopupUrl != null &&
!(window as any).previousPopupUrl.startsWith("/ciphers");
}
async ngOnInit() {
this.searchTypeSearch = !this.platformUtilsService.isSafari();
this.showOrganizations = await this.organizationService.hasOrganizations();
this.vaultFilter = this.vaultFilterService.getVaultFilter();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (this.applySavedState) {
this.state = await this.stateService.getBrowserVaultItemsComponentState();
if (this.state?.searchText) {
this.searchText = this.state.searchText;
}
}
if (params.deleted) {
this.showVaultFilter = true;
this.groupingTitle = this.i18nService.t("trash");
this.searchPlaceholder = this.i18nService.t("searchTrash");
await this.load(this.buildFilter(), true);
} else if (params.type) {
this.showVaultFilter = true;
this.searchPlaceholder = this.i18nService.t("searchType");
this.type = parseInt(params.type, null);
switch (this.type) {
case CipherType.Login:
this.groupingTitle = this.i18nService.t("logins");
break;
case CipherType.Card:
this.groupingTitle = this.i18nService.t("cards");
break;
case CipherType.Identity:
this.groupingTitle = this.i18nService.t("identities");
break;
case CipherType.SecureNote:
this.groupingTitle = this.i18nService.t("secureNotes");
break;
case CipherType.SshKey:
this.groupingTitle = this.i18nService.t("sshKeys");
break;
default:
break;
}
await this.load(this.buildFilter());
} else if (params.folderId) {
this.showVaultFilter = true;
this.folderId = params.folderId === "none" ? null : params.folderId;
this.searchPlaceholder = this.i18nService.t("searchFolder");
if (this.folderId != null) {
this.showOrganizations = false;
const folderNode = await this.vaultFilterService.getFolderNested(this.folderId);
if (folderNode != null && folderNode.node != null) {
this.groupingTitle = folderNode.node.name;
this.nestedFolders =
folderNode.children != null && folderNode.children.length > 0
? folderNode.children
: null;
}
} else {
this.noneFolder = true;
this.groupingTitle = this.i18nService.t("noneFolder");
}
await this.load(this.buildFilter());
} else if (params.collectionId) {
this.showVaultFilter = false;
this.collectionId = params.collectionId;
this.searchPlaceholder = this.i18nService.t("searchCollection");
const collectionNode = await this.collectionService.getNested(this.collectionId);
if (collectionNode != null && collectionNode.node != null) {
this.groupingTitle = collectionNode.node.name;
this.nestedCollections =
collectionNode.children != null && collectionNode.children.length > 0
? collectionNode.children
: null;
}
await this.load(
(c) => c.collectionIds != null && c.collectionIds.indexOf(this.collectionId) > -1,
);
} else {
this.showVaultFilter = true;
this.groupingTitle = this.i18nService.t("allItems");
await this.load(this.buildFilter());
}
if (this.applySavedState && this.state != 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
BrowserPopupUtils.setContentScrollY(window, this.state.scrollY, {
delay: 0,
containerSelector: this.scrollingContainer,
});
}
await this.stateService.setBrowserVaultItemsComponentState(null);
});
this.broadcasterService.subscribe(ComponentId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "syncCompleted":
if (message.successfully) {
window.setTimeout(() => {
// 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.refresh();
}, 500);
}
break;
default:
break;
}
this.changeDetectorRef.detectChanges();
});
});
}
ngOnDestroy() {
// 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.saveState();
this.broadcasterService.unsubscribe(ComponentId);
}
selectCipher(cipher: CipherView) {
this.selectedTimeout = window.setTimeout(() => {
if (!this.preventSelected) {
super.selectCipher(cipher);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/view-cipher"], {
queryParams: { cipherId: cipher.id, collectionId: this.collectionId },
});
}
this.preventSelected = false;
}, 200);
}
selectFolder(folder: FolderView) {
if (folder.id != null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/ciphers"], { queryParams: { folderId: folder.id } });
}
}
selectCollection(collection: CollectionView) {
// 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(["/ciphers"], { queryParams: { collectionId: collection.id } });
}
async launchCipher(cipher: CipherView) {
if (cipher.type !== CipherType.Login || !cipher.login.canLaunch) {
return;
}
if (this.selectedTimeout != null) {
window.clearTimeout(this.selectedTimeout);
}
this.preventSelected = true;
await this.cipherService.updateLastLaunchedDate(cipher.id);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.createNewTab(cipher.login.launchUri);
if (BrowserPopupUtils.inPopup(window)) {
BrowserApi.closePopup(window);
}
}
addCipher() {
if (this.deleted) {
return false;
}
super.addCipher();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-cipher"], {
queryParams: {
folderId: this.folderId,
type: this.type,
collectionId: this.collectionId,
selectedVault: this.vaultFilter.selectedOrganizationId,
},
});
}
back() {
(window as any).routeDirection = "b";
this.location.back();
}
showGroupings() {
return (
!this.isSearching() &&
((this.nestedFolders && this.nestedFolders.length) ||
(this.nestedCollections && this.nestedCollections.length))
);
}
async changeVaultSelection() {
this.vaultFilter = this.vaultFilterService.getVaultFilter();
await this.load(this.buildFilter(), this.deleted);
}
private buildFilter(): (cipher: CipherView) => boolean {
return (cipher) => {
let cipherPassesFilter = true;
if (this.deleted && cipherPassesFilter) {
cipherPassesFilter = cipher.isDeleted;
}
if (this.type != null && cipherPassesFilter) {
cipherPassesFilter = cipher.type === this.type;
}
if (this.folderId != null && this.folderId != "none" && cipherPassesFilter) {
cipherPassesFilter = cipher.folderId === this.folderId;
}
if (this.noneFolder) {
cipherPassesFilter = cipher.folderId == null;
}
if (this.collectionId != null && cipherPassesFilter) {
cipherPassesFilter =
cipher.collectionIds != null && cipher.collectionIds.indexOf(this.collectionId) > -1;
}
if (this.vaultFilter.selectedOrganizationId != null && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === this.vaultFilter.selectedOrganizationId;
}
if (this.vaultFilter.myVaultOnly && cipherPassesFilter) {
cipherPassesFilter = cipher.organizationId === null;
}
return cipherPassesFilter;
};
}
private async saveState() {
this.state = {
scrollY: BrowserPopupUtils.getContentScrollY(window, this.scrollingContainer),
searchText: this.searchText,
};
await this.stateService.setBrowserVaultItemsComponentState(this.state);
}
}

View File

@@ -1,82 +0,0 @@
<ng-container *ngIf="loaded && organizations$ | async as organizations">
<div class="content org-filter-content" *ngIf="loaded && shouldShow(organizations)">
<ng-container *ngIf="selectedVault$ | async as vaultFilterDisplay">
<button
type="button"
#toggleVaults
class="org-filter"
(click)="openOverlay()"
aria-haspopup="menu"
aria-controls="cdk-overlay-container"
[attr.aria-expanded]="isOpen"
[attr.aria-label]="vaultFilterDisplay"
>
<span class="org-filter-text-container">
<span class="org-filter-text-name">{{ vaultFilterDisplay }}</span
>&nbsp;
<span
><i
class="bwi bwi-sm"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-down': !isOpen, 'bwi-angle-up': isOpen }"
></i></span
></span>
</button>
</ng-container>
<ng-template class="vault-select-container" #vaultSelectorTemplate>
<div
class="vault-select"
[@transformPanel]="'open'"
cdkTrapFocus
cdkTrapFocusAutoCapture
role="dialog"
aria-modal="true"
>
<button type="button" appStopClick (click)="selectAllVaults()">
<div class="vault-select-org-text-container">
<i class="bwi bwi-fw bwi-filter vault-select-prefix-icon" aria-hidden="true"></i>
<span class="vault-select-org-name">{{ "allVaults" | i18n }}</span>
</div>
</button>
<button
type="button"
*ngIf="!enforcePersonalOwnership"
appStopClick
(click)="selectMyVault()"
>
<div class="vault-select-org-text-container">
<i class="bwi bwi-fw bwi-user vault-select-prefix-icon" aria-hidden="true"></i>
<span class="vault-select-org-name">{{ "myVault" | i18n }}</span>
</div>
</button>
<button
type="button"
*ngFor="let organization of organizations"
appStopClick
(click)="selectOrganization(organization)"
>
<div class="vault-select-org-text-container">
<i
*ngIf="organization.productTierType !== 1"
class="bwi bwi-fw bwi-business vault-select-prefix-icon"
aria-hidden="true"
></i>
<i
*ngIf="organization.productTierType === 1"
class="bwi bwi-fw bwi-family vault-select-prefix-icon"
aria-hidden="true"
></i>
<span class="vault-select-org-name">{{ organization.name }}</span
><i
*ngIf="!organization.enabled"
class="bwi bwi-fw bwi-exclamation-triangle text-danger vault-select-suffix-icon"
attr.aria-label="{{ 'organizationIsDisabled' | i18n }}"
appA11yTitle="{{ 'organizationIsDisabled' | i18n }}"
></i>
</div>
</button>
</div>
</ng-template>
</div>
</ng-container>

View File

@@ -1,227 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { animate, state, style, transition, trigger } from "@angular/animations";
import { ConnectedPosition, Overlay, OverlayRef } from "@angular/cdk/overlay";
import { TemplatePortal } from "@angular/cdk/portal";
import {
Component,
ElementRef,
EventEmitter,
HostListener,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild,
ViewContainerRef,
} from "@angular/core";
import {
BehaviorSubject,
combineLatest,
concatMap,
map,
merge,
Observable,
Subject,
takeUntil,
} from "rxjs";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { VaultFilterService } from "../../../services/vault-filter.service";
@Component({
selector: "app-vault-select",
templateUrl: "vault-select.component.html",
animations: [
trigger("transformPanel", [
state(
"void",
style({
opacity: 0,
}),
),
transition(
"void => open",
animate(
"100ms linear",
style({
opacity: 1,
}),
),
),
transition("* => void", animate("100ms linear", style({ opacity: 0 }))),
]),
],
})
export class VaultSelectComponent implements OnInit, OnDestroy {
@Output() onVaultSelectionChanged = new EventEmitter();
@ViewChild("toggleVaults", { read: ElementRef })
buttonRef: ElementRef<HTMLButtonElement>;
@ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef<HTMLElement>;
private _selectedVault = new BehaviorSubject<string | null>(null);
isOpen = false;
loaded = false;
organizations$: Observable<Organization[]>;
selectedVault$: Observable<string | null> = this._selectedVault.asObservable();
enforcePersonalOwnership = false;
overlayPosition: ConnectedPosition[] = [
{
originX: "start",
originY: "bottom",
overlayX: "start",
overlayY: "top",
},
];
private overlayRef: OverlayRef;
private _destroy = new Subject<void>();
shouldShow(organizations: Organization[]): boolean {
return (
(organizations.length > 0 && !this.enforcePersonalOwnership) ||
(organizations.length > 1 && this.enforcePersonalOwnership)
);
}
constructor(
private vaultFilterService: VaultFilterService,
private i18nService: I18nService,
private overlay: Overlay,
private viewContainerRef: ViewContainerRef,
private platformUtilsService: PlatformUtilsService,
private organizationService: OrganizationService,
private policyService: PolicyService,
) {}
@HostListener("document:keydown.escape", ["$event"])
handleKeyboardEvent(event: KeyboardEvent) {
if (this.isOpen) {
event.preventDefault();
this.close();
}
}
async ngOnInit() {
this.organizations$ = this.organizationService.memberOrganizations$
.pipe(takeUntil(this._destroy))
.pipe(map((orgs) => orgs.sort(Utils.getSortFunction(this.i18nService, "name"))));
combineLatest([
this.organizations$,
this.policyService.policyAppliesToActiveUser$(PolicyType.PersonalOwnership),
])
.pipe(
concatMap(async ([organizations, enforcePersonalOwnership]) => {
this.enforcePersonalOwnership = enforcePersonalOwnership;
if (this.shouldShow(organizations)) {
if (this.enforcePersonalOwnership && !this.vaultFilterService.vaultFilter.myVaultOnly) {
const firstOrganization = organizations[0];
this._selectedVault.next(firstOrganization.name);
this.vaultFilterService.setVaultFilter(firstOrganization.id);
} else if (this.vaultFilterService.vaultFilter.myVaultOnly) {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
} else if (this.vaultFilterService.vaultFilter.selectedOrganizationId != null) {
const selectedOrganization = organizations.find(
(o) => o.id === this.vaultFilterService.vaultFilter.selectedOrganizationId,
);
this._selectedVault.next(selectedOrganization.name);
} else {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
}
}
}),
)
.pipe(takeUntil(this._destroy))
.subscribe();
this.loaded = true;
}
ngOnDestroy(): void {
this._destroy.next();
this._destroy.complete();
this._selectedVault.complete();
}
openOverlay() {
const viewPortHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
const positionStrategyBuilder = this.overlay.position();
const positionStrategy = positionStrategyBuilder
.flexibleConnectedTo(this.buttonRef.nativeElement)
.withFlexibleDimensions(true)
.withPush(true)
.withViewportMargin(10)
.withGrowAfterOpen(true)
.withPositions(this.overlayPosition);
this.overlayRef = this.overlay.create({
hasBackdrop: true,
positionStrategy,
maxHeight: viewPortHeight - 160,
backdropClass: "cdk-overlay-transparent-backdrop",
scrollStrategy: this.overlay.scrollStrategies.close(),
});
const templatePortal = new TemplatePortal(this.templateRef, this.viewContainerRef);
this.overlayRef.attach(templatePortal);
this.isOpen = true;
// Handle closing
merge(
this.overlayRef.outsidePointerEvents(),
this.overlayRef.backdropClick(),
this.overlayRef.detachments(),
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
).subscribe(() => {
this.close();
});
}
close() {
if (this.overlayRef) {
this.overlayRef.dispose();
this.overlayRef = undefined;
}
this.isOpen = false;
}
selectOrganization(organization: Organization) {
if (!organization.enabled) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("disabledOrganizationFilterError"),
);
} else {
this._selectedVault.next(organization.name);
this.vaultFilterService.setVaultFilter(organization.id);
this.onVaultSelectionChanged.emit();
this.close();
}
}
selectAllVaults() {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults);
this.onVaultSelectionChanged.emit();
this.close();
}
selectMyVault() {
this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault));
this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault);
this.onVaultSelectionChanged.emit();
this.close();
}
}

View File

@@ -1,98 +0,0 @@
<ng-container>
<h2 class="box-header">
{{ "customFields" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row box-content-row-flex" *ngFor="let field of cipher.fields">
<div class="row-main">
<span
*ngIf="field.type != fieldType.Linked"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, field.value)"
>{{ field.name }}</span
>
<span *ngIf="field.type === fieldType.Linked" class="row-label">{{ field.name }}</span>
<div *ngIf="field.type === fieldType.Text">
{{ field.value || "&nbsp;" }}
</div>
<div *ngIf="field.type === fieldType.Hidden">
<span *ngIf="!field.showValue" class="monospaced">{{ field.maskedValue }}</span>
<span
*ngIf="field.showValue && !field.showCount"
class="monospaced show-whitespace"
[innerHTML]="field.value | colorPassword"
></span>
<span
*ngIf="field.showValue && field.showCount"
[innerHTML]="field.value | colorPasswordCount"
></span>
</div>
<div *ngIf="field.type === fieldType.Boolean">
<i class="bwi bwi-check-square" *ngIf="field.value === 'true'" aria-hidden="true"></i>
<i class="bwi bwi-square" *ngIf="field.value !== 'true'" aria-hidden="true"></i>
<span class="sr-only">{{ field.value }}</span>
</div>
<div *ngIf="field.type === fieldType.Linked" class="box-content-row-flex">
<div class="icon icon-small">
<i
class="bwi bwi-link"
aria-hidden="true"
appA11yTitle="{{ 'linkedValue' | i18n }}"
></i>
<span class="sr-only">{{ "linkedValue" | i18n }}</span>
</div>
<span>{{ cipher.linkedFieldI18nKey(field.linkedId) | i18n }}</span>
</div>
</div>
<div class="action-buttons action-buttons-fixed">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword && field.showValue"
(click)="toggleFieldCount(field)"
[attr.aria-pressed]="field.showCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
*ngIf="field.type === fieldType.Hidden && cipher.viewPassword"
(click)="toggleFieldValue(field)"
[attr.aria-pressed]="field.showValue"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !field.showValue, 'bwi-eye-slash': field.showValue }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'copyValue' | i18n }} {{ field.name }}"
appA11yTitle="{{ 'copyValue' | i18n }}"
*ngIf="
field.value &&
field.type !== fieldType.Boolean &&
field.type !== fieldType.Linked &&
!(field.type === fieldType.Hidden && !cipher.viewPassword)
"
(click)="
copy(field.value, 'value', field.type === fieldType.Hidden ? 'H_Field' : 'Field')
"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</ng-container>

View File

@@ -1,14 +0,0 @@
import { Component } from "@angular/core";
import { ViewCustomFieldsComponent as BaseViewCustomFieldsComponent } from "@bitwarden/angular/vault/components/view-custom-fields.component";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
@Component({
selector: "app-vault-view-custom-fields",
templateUrl: "view-custom-fields.component.html",
})
export class ViewCustomFieldsComponent extends BaseViewCustomFieldsComponent {
constructor(eventCollectionService: EventCollectionService) {
super(eventCollectionService);
}
}

View File

@@ -1,719 +0,0 @@
<header>
<div class="left">
<button type="button" (click)="close()">{{ "close" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ "viewItem" | i18n }}</span>
</h1>
<div class="right" *ngIf="cipher">
<button type="button" (click)="edit()" *ngIf="!cipher.isDeleted">
{{ "edit" | i18n }}
</button>
</div>
</header>
<main tabindex="-1" *ngIf="cipher">
<div class="box">
<h2 class="box-header">
{{ "itemInformation" | i18n }}
</h2>
<div class="box-content">
<div class="box-content-row">
<label
for="name"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.name)"
>{{ "name" | i18n }}</label
>
<input id="name" type="text" [value]="cipher.name" readonly aria-readonly="true" />
</div>
<!-- Login -->
<div *ngIf="cipher.login">
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.username">
<div class="row-main">
<label
for="loginUsername"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.login.username)"
>{{ "username" | i18n }}
</label>
<input
id="loginUsername"
type="text"
[value]="cipher.login.username"
readonly
aria-readonly="true"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyUsername' | i18n }}"
(click)="copy(cipher.login.username, 'username', 'Username')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.login.password">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.login.password)"
>{{ "password" | i18n }}</span
>
<div *ngIf="!showPassword" class="monospaced">
{{ cipher.login.maskedPassword }}
</div>
<div
*ngIf="showPassword && !showPasswordCount"
class="monospaced password-wrapper"
[appCopyText]="cipher.login.password"
[innerHTML]="cipher.login.password | colorPassword"
></div>
<div
*ngIf="showPassword && showPasswordCount"
[innerHTML]="cipher.login.password | colorPasswordCount"
></div>
</div>
<div class="action-buttons action-buttons-fixed">
<button
type="button"
#checkPasswordBtn
class="row-btn btn"
appA11yTitle="{{ 'checkPassword' | i18n }}"
(click)="checkPassword()"
[appApiAction]="checkPasswordPromise"
[disabled]="$any(checkPasswordBtn).loading"
*ngIf="cipher.viewPassword"
>
<i
class="bwi bwi-lg bwi-check-circle"
[hidden]="$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
<i
class="bwi bwi-lg bwi-spinner bwi-spin"
[hidden]="!$any(checkPasswordBtn).loading"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleCharacterCount' | i18n }} {{ 'password' | i18n }}"
appA11yTitle="{{ 'toggleCharacterCount' | i18n }}"
(click)="togglePasswordCount()"
*ngIf="showPassword"
[attr.aria-pressed]="showPasswordCount"
>
<i class="bwi bwi-lg bwi-numbered-list" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'password' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="togglePassword()"
*ngIf="cipher.viewPassword"
[attr.aria-pressed]="showPassword"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyPassword' | i18n }}"
(click)="copy(cipher.login.password, 'password', 'Password')"
*ngIf="cipher.viewPassword"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<!--Passkey-->
<div
class="box"
*ngIf="cipher.login.hasFido2Credentials"
tabindex="0"
attr.aria-label="{{ 'typePasskey' | i18n }} {{ fido2CredentialCreationDateValue }}"
>
<div class="box-content">
<div class="box-content-row text-muted">
<span class="row-label">{{ "typePasskey" | i18n }}</span>
{{ fido2CredentialCreationDateValue }}
</div>
</div>
</div>
<div
class="box-content-row box-content-row-flex totp"
[ngClass]="{ low: totpLow }"
*ngIf="cipher.login.totp && totpCode"
>
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, totpCode)"
>{{ "verificationCodeTotp" | i18n }}</span
>
<span class="totp-code">{{ totpCodeFormatted }}</span>
</div>
<span class="totp-countdown" aria-hidden="true">
<span class="totp-sec">{{ totpSec }}</span>
<svg>
<g>
<circle
class="totp-circle inner"
r="12.6"
cy="16"
cx="16"
[ngStyle]="{ 'stroke-dashoffset.px': totpDash }"
></circle>
<circle class="totp-circle outer" r="14" cy="16" cx="16"></circle>
</g>
</svg>
</span>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
title="{{ 'copyVerificationCode' | i18n }}"
(click)="copy(totpCode, 'verificationCodeTotp', 'TOTP')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
<span class="sr-only">{{ "copyValue" | i18n }}</span>
<span
class="sr-only exists-only-on-parent-focus"
aria-live="polite"
aria-atomic="true"
>{{ totpSec }}</span
>
</button>
</div>
</div>
<div class="box-content-row box-content-row-flex totp" *ngIf="showPremiumRequiredTotp">
<div class="row-main">
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
<span class="row-label">
<a routerLink="/premium">
{{ "premiumSubcriptionRequired" | i18n }}
</a>
</span>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.card">
<div class="box-content-row" *ngIf="cipher.card.cardholderName">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.cardholderName)"
>{{ "cardholderName" | i18n }}</span
>
{{ cipher.card.cardholderName }}
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.number">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.number)"
>{{ "number" | i18n }}</span
>
<span [hidden]="showCardNumber" class="monospaced">{{
cipher.card.maskedNumber | creditCardNumber: cipher.card.brand
}}</span>
<span [hidden]="!showCardNumber" class="monospaced">{{
cipher.card.number | creditCardNumber: cipher.card.brand
}}</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'number' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardNumber()"
[attr.aria-pressed]="showCardNumber"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardNumber, 'bwi-eye-slash': showCardNumber }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copyNumber' | i18n }}"
(click)="copy(cipher.card.number, 'number', 'Card Number')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="box-content-row" *ngIf="cipher.card.brand">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.brand)"
>{{ "brand" | i18n }}</span
>
{{ cipher.card.brand }}
</div>
<div class="box-content-row" *ngIf="cipher.card.expiration">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.expiration)"
>{{ "expiration" | i18n }}</span
>
{{ cipher.card.expiration }}
</div>
<div class="box-content-row box-content-row-flex" *ngIf="cipher.card.code">
<div class="row-main">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.card.code)"
>{{ "securityCode" | i18n }}</span
>
<span [hidden]="showCardCode" class="monospaced">{{ cipher.card.maskedCode }}</span>
<span [hidden]="!showCardCode" class="monospaced">{{ cipher.card.code }}</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'toggleVisibility' | i18n }} {{ 'securityCode' | i18n }}"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleCardCode()"
[attr.aria-pressed]="showCardCode"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showCardCode, 'bwi-eye-slash': showCardCode }"
></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'copySecurityCode' | i18n }}"
(click)="copy(cipher.card.code, 'securityCode', 'Security Code')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Identity -->
<div *ngIf="cipher.identity">
<div class="box-content-row" *ngIf="cipher.identity.fullName">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.fullName)"
>{{ "identityName" | i18n }}</span
>
{{ cipher.identity.fullName }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.username">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.username)"
>{{ "username" | i18n }}</span
>
{{ cipher.identity.username }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.company">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.company)"
>{{ "company" | i18n }}</span
>
{{ cipher.identity.company }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.ssn">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.ssn)"
>{{ "ssn" | i18n }}</span
>
{{ cipher.identity.ssn }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.passportNumber">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.passportNumber)"
>{{ "passportNumber" | i18n }}</span
>
{{ cipher.identity.passportNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.licenseNumber">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.licenseNumber)"
>{{ "licenseNumber" | i18n }}</span
>
{{ cipher.identity.licenseNumber }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.email">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.email)"
>{{ "email" | i18n }}</span
>
{{ cipher.identity.email }}
</div>
<div class="box-content-row" *ngIf="cipher.identity.phone">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.identity.phone)"
>{{ "phone" | i18n }}</span
>
{{ cipher.identity.phone }}
</div>
<div
class="box-content-row"
*ngIf="cipher.identity.address1 || cipher.identity.city || cipher.identity.country"
>
<span
class="row-label draggable"
draggable="true"
(dragstart)="
setTextDataOnDrag(
$event,
(cipher.identity.address1 ? cipher.identity.address1 + '\n' : '') +
(cipher.identity.address2 ? cipher.identity.address2 + '\n' : '') +
(cipher.identity.address3 ? cipher.identity.address3 + '\n' : '') +
(cipher.identity.fullAddressPart2
? cipher.identity.fullAddressPart2 + '\n'
: '') +
(cipher.identity.country ? cipher.identity.country : '')
)
"
>{{ "address" | i18n }}</span
>
<div *ngIf="cipher.identity.address1">{{ cipher.identity.address1 }}</div>
<div *ngIf="cipher.identity.address2">{{ cipher.identity.address2 }}</div>
<div *ngIf="cipher.identity.address3">{{ cipher.identity.address3 }}</div>
<div *ngIf="cipher.identity.fullAddressPart2">{{ cipher.identity.fullAddressPart2 }}</div>
<div *ngIf="cipher.identity.country">{{ cipher.identity.country }}</div>
</div>
</div>
<!-- SshKey -->
<div *ngIf="cipher.sshKey">
<div class="box-content-row" *ngIf="cipher.sshKey.privateKey" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.privateKey)"
>
{{ "sshPrivateKey" | i18n }}
</span>
<div [innerText]="cipher.sshKey.maskedPrivateKey" class="monospaced"></div>
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.publicKey" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.publicKey)"
>
{{ "sshPublicKey" | i18n }}</span
>
{{ cipher.sshKey.publicKey }}
</div>
<div class="box-content-row" *ngIf="cipher.sshKey.keyFingerprint" style="overflow: hidden">
<span
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.sshKey.keyFingerprint)"
>
{{ "sshFingerprint" | i18n }}</span
>
{{ cipher.sshKey.keyFingerprint }}
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.login && cipher.login.hasUris">
<div class="box-content">
<div
class="box-content-row box-content-row-flex"
*ngFor="let u of cipher.login.uris; let i = index"
>
<div class="row-main">
<label
for="hostOrUri{{ i }}"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, u.uri)"
*ngIf="!u.isWebsite"
>{{ "uri" | i18n }}</label
>
<label
for="hostOrUri{{ i }}"
class="row-label draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, u.uri)"
*ngIf="u.isWebsite"
>{{ "website" | i18n }}</label
>
<span title="{{ u.uri }}">
<input
id="hostOrUri{{ i }}"
type="text"
[value]="u.hostOrUri"
readonly
aria-readonly="true"
/>
</span>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'launch' | i18n }} {{ u.uri }}"
appA11yTitle="{{ 'launch' | i18n }}"
*ngIf="u.canLaunch"
(click)="launch(u, cipher.id)"
>
<i class="bwi bwi-lg bwi-share-square" aria-hidden="true"></i>
</button>
<button
type="button"
class="row-btn"
appStopClick
attr.aria-label="{{ 'copyUri' | i18n }} {{ u.uri }}"
appA11yTitle="{{ 'copyUri' | i18n }}"
(click)="copy(u.uri, u.isWebsite ? 'website' : 'uri', 'URI')"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.folderId && folder">
<div class="box-content">
<div class="box-content-row">
<label
for="folderName"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, folder.name)"
>{{ "folder" | i18n }}</label
>
<input id="folderName" type="text" name="folderName" [value]="folder.name" readonly />
</div>
</div>
</div>
<div class="box" *ngIf="cipher.notes">
<h2 class="box-header">
<label
for="notes"
class="draggable"
draggable="true"
(dragstart)="setTextDataOnDrag($event, cipher.notes)"
>{{ "notes" | i18n }}</label
>
</h2>
<div class="box-content">
<div class="box-content-row">
<textarea
id="notes"
[value]="cipher.notes"
rows="6"
readonly
aria-readonly="true"
></textarea>
</div>
</div>
</div>
<div class="box" *ngIf="cipher.hasFields">
<app-vault-view-custom-fields
[cipher]="cipher"
[promptPassword]="promptPassword.bind(this)"
[copy]="copy.bind(this)"
></app-vault-view-custom-fields>
</div>
<div
class="box"
*ngIf="cipher.hasAttachments && (canAccessPremium || cipher.organizationId) && showAttachments"
>
<h2 class="box-header">
{{ "attachments" | i18n }}
</h2>
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
*ngFor="let attachment of cipher.attachments"
appStopClick
(click)="downloadAttachment(attachment)"
>
<span class="row-main">{{ attachment.fileName }}</span>
<small class="row-sub-label">{{ attachment.sizeName }}</small>
<i
class="bwi bwi-download bwi-fw row-sub-icon"
*ngIf="!$any(attachment).downloading"
aria-hidden="true"
></i>
<i
class="bwi bwi-spinner bwi-fw bwi-spin row-sub-icon"
*ngIf="$any(attachment).downloading"
aria-hidden="true"
></i>
</button>
</div>
</div>
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="fillCipher()"
*ngIf="
cipher.type !== cipherType.SecureNote &&
!cipher.isDeleted &&
(!this.inPopout || this.loadAction)
"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-pencil-square bwi-lg bwi-fw"></i>
</div>
<span>{{ "autoFill" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="fillCipherAndSave()"
*ngIf="cipher.type === cipherType.Login && !cipher.isDeleted && !inPopout"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-bookmark bwi-lg bwi-fw"></i>
</div>
<span>{{ "autoFillAndSave" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="clone()"
*ngIf="!cipher.organizationId && !cipher.isDeleted"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-files bwi-lg bwi-fw"></i>
</div>
<span>{{ "cloneItem" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="share()"
*ngIf="!cipher.organizationId"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-arrow-circle-right bwi-lg bwi-fw"></i>
</div>
<span>{{ "moveToOrganization" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="restore()"
*ngIf="cipher.isDeleted"
>
<div class="row-main text-primary">
<div class="icon text-primary" aria-hidden="true">
<i class="bwi bwi-undo bwi-lg bwi-fw"></i>
</div>
<span>{{ "restoreItem" | i18n }}</span>
</div>
</button>
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
*ngIf="canDeleteCipher$ | async"
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw"></i>
</div>
<span>{{ (cipher.isDeleted ? "permanentlyDeleteItem" : "deleteItem") | i18n }}</span>
</div>
</button>
</div>
</div>
<div class="box">
<div class="box-footer">
<div>
<b class="font-weight-semibold">{{ "dateUpdated" | i18n }}:</b>
{{ cipher.revisionDate | date: "medium" }}
</div>
<div *ngIf="cipher.creationDate">
<b class="font-weight-semibold">{{ "dateCreated" | i18n }}:</b>
{{ cipher.creationDate | date: "medium" }}
</div>
<div *ngIf="cipher.passwordRevisionDisplayDate">
<b class="font-weight-semibold">{{ "datePasswordUpdated" | i18n }}:</b>
{{ cipher.passwordRevisionDisplayDate | date: "medium" }}
</div>
<div *ngIf="cipher.hasPasswordHistory">
<b class="font-weight-semibold">{{ "passwordHistory" | i18n }}:</b>
<button
type="button"
routerLink="/cipher-password-history"
[queryParams]="{ cipherId: cipher.id }"
appStopClick
title="{{ 'passwordHistory' | i18n }}"
>
{{ cipher.passwordHistory.length }}
</button>
</div>
</div>
</div>
</main>

View File

@@ -1,443 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { DatePipe, Location } from "@angular/common";
import { ChangeDetectorRef, Component, NgZone, OnInit, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { Subject, firstValueFrom, takeUntil, Subscription } from "rxjs";
import { first, map } from "rxjs/operators";
import { ViewComponent as BaseViewComponent } from "@bitwarden/angular/vault/components/view.component";
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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { PasswordRepromptService } from "@bitwarden/vault";
import { BrowserFido2UserInterfaceSession } from "../../../../autofill/fido2/services/browser-fido2-user-interface.service";
import { AutofillService } from "../../../../autofill/services/abstractions/autofill.service";
import { BrowserApi } from "../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils";
import { fido2PopoutSessionData$ } from "../../utils/fido2-popout-session-data";
import { closeViewVaultItemPopout, VaultPopoutType } from "../../utils/vault-popout-window";
const BroadcasterSubscriptionId = "ChildViewComponent";
export const AUTOFILL_ID = "autofill";
export const SHOW_AUTOFILL_BUTTON = "show-autofill-button";
export const COPY_USERNAME_ID = "copy-username";
export const COPY_PASSWORD_ID = "copy-password";
export const COPY_VERIFICATION_CODE_ID = "copy-totp";
type CopyAction =
| typeof COPY_USERNAME_ID
| typeof COPY_PASSWORD_ID
| typeof COPY_VERIFICATION_CODE_ID;
type LoadAction = typeof AUTOFILL_ID | typeof SHOW_AUTOFILL_BUTTON | CopyAction;
@Component({
selector: "app-vault-view",
templateUrl: "view.component.html",
})
export class ViewComponent extends BaseViewComponent implements OnInit, OnDestroy {
showAttachments = true;
pageDetails: any[] = [];
tab: any;
senderTabId?: number;
loadAction?: LoadAction;
private static readonly copyActions = new Set([
COPY_USERNAME_ID,
COPY_PASSWORD_ID,
COPY_VERIFICATION_CODE_ID,
]);
uilocation?: "popout" | "popup" | "sidebar" | "tab";
loadPageDetailsTimeout: number;
inPopout = false;
cipherType = CipherType;
private fido2PopoutSessionData$ = fido2PopoutSessionData$();
private collectPageDetailsSubscription: Subscription;
private destroy$ = new Subject<void>();
constructor(
cipherService: CipherService,
folderService: FolderService,
totpService: TotpServiceAbstraction,
tokenService: TokenService,
i18nService: I18nService,
keyService: KeyService,
encryptService: EncryptService,
platformUtilsService: PlatformUtilsService,
auditService: AuditService,
private route: ActivatedRoute,
private router: Router,
private location: Location,
broadcasterService: BroadcasterService,
ngZone: NgZone,
changeDetectorRef: ChangeDetectorRef,
stateService: StateService,
eventCollectionService: EventCollectionService,
private autofillService: AutofillService,
private messagingService: MessagingService,
apiService: ApiService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
datePipe: DatePipe,
accountService: AccountService,
billingAccountProfileStateService: BillingAccountProfileStateService,
cipherAuthorizationService: CipherAuthorizationService,
) {
super(
cipherService,
folderService,
totpService,
tokenService,
i18nService,
keyService,
encryptService,
platformUtilsService,
auditService,
window,
broadcasterService,
ngZone,
changeDetectorRef,
eventCollectionService,
apiService,
passwordRepromptService,
logService,
stateService,
fileDownloadService,
dialogService,
datePipe,
accountService,
billingAccountProfileStateService,
cipherAuthorizationService,
);
}
ngOnInit() {
this.route.queryParams.pipe(takeUntil(this.destroy$)).subscribe((value) => {
this.loadAction = value?.action;
this.senderTabId = parseInt(value?.senderTabId, 10) || undefined;
this.uilocation = value?.uilocation;
});
this.inPopout = this.uilocation === "popout" || BrowserPopupUtils.inPopout(window);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.cipherId) {
this.cipherId = params.cipherId;
}
if (params.collectionId) {
this.collectionId = params.collectionId;
}
if (!params.cipherId) {
// 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.close();
}
await this.load();
});
super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.ngZone.run(async () => {
switch (message.command) {
case "tabChanged":
case "windowChanged":
if (this.loadPageDetailsTimeout != null) {
window.clearTimeout(this.loadPageDetailsTimeout);
}
this.loadPageDetailsTimeout = window.setTimeout(() => this.loadPageDetails(), 500);
break;
default:
break;
}
});
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
async load() {
await super.load();
await this.loadPageDetails();
await this.handleLoadAction();
}
async edit() {
if (this.cipher.isDeleted) {
return false;
}
if (!(await super.edit())) {
return false;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/edit-cipher"], {
queryParams: {
cipherId: this.cipher.id,
type: this.cipher.type,
isNew: false,
collectionId: this.collectionId,
},
});
return true;
}
async clone() {
if (this.cipher.isDeleted) {
return false;
}
if (!(await super.clone())) {
return false;
}
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/clone-cipher"], {
queryParams: {
cloneMode: true,
cipherId: this.cipher.id,
},
});
return true;
}
async share() {
if (!(await super.share())) {
return false;
}
if (this.cipher.organizationId == null) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/share-cipher"], {
replaceUrl: true,
queryParams: { cipherId: this.cipher.id },
});
}
return true;
}
async fillCipher() {
const didAutofill = await this.doAutofill();
if (didAutofill) {
this.platformUtilsService.showToast("success", null, this.i18nService.t("autoFillSuccess"));
}
return didAutofill;
}
async fillCipherAndSave() {
const didAutofill = await this.doAutofill();
if (didAutofill) {
if (this.tab == null) {
throw new Error("No tab found.");
}
if (this.cipher.login.uris == null) {
this.cipher.login.uris = [];
} else {
if (this.cipher.login.uris.some((uri) => uri.uri === this.tab.url)) {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("autoFillSuccessAndSavedUri"),
);
return;
}
}
const loginUri = new LoginUriView();
loginUri.uri = this.tab.url;
this.cipher.login.uris.push(loginUri);
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipher: Cipher = await this.cipherService.encrypt(this.cipher, activeUserId);
await this.cipherService.updateWithServer(cipher);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("autoFillSuccessAndSavedUri"),
);
this.messagingService.send("editedCipher");
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
}
}
async restore() {
if (!this.cipher.isDeleted) {
return false;
}
if (await super.restore()) {
// 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.close();
return true;
}
return false;
}
async delete() {
if (await super.delete()) {
this.messagingService.send("deletedCipher");
// 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.close();
return true;
}
return false;
}
async close() {
const sessionData = await firstValueFrom(this.fido2PopoutSessionData$);
if (this.inPopout && sessionData.isFido2Session) {
BrowserFido2UserInterfaceSession.abortPopout(sessionData.sessionId);
return;
}
if (
BrowserPopupUtils.inSingleActionPopout(window, VaultPopoutType.viewVaultItem) &&
this.senderTabId
) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
BrowserApi.focusTab(this.senderTabId);
// 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
closeViewVaultItemPopout(`${VaultPopoutType.viewVaultItem}_${this.cipher.id}`);
return;
}
this.location.back();
}
private async loadPageDetails() {
this.collectPageDetailsSubscription?.unsubscribe();
this.pageDetails = [];
this.tab = this.senderTabId
? await BrowserApi.getTab(this.senderTabId)
: await BrowserApi.getTabFromCurrentWindow();
if (!this.tab) {
return;
}
this.collectPageDetailsSubscription = this.autofillService
.collectPageDetailsFromTab$(this.tab)
.pipe(takeUntil(this.destroy$))
.subscribe((pageDetails) => (this.pageDetails = pageDetails));
}
private async doAutofill() {
const originalTabURL = this.tab.url?.length && new URL(this.tab.url);
if (!(await this.promptPassword())) {
return false;
}
const currentTabURL = this.tab.url?.length && new URL(this.tab.url);
const originalTabHostPath =
originalTabURL && `${originalTabURL.origin}${originalTabURL.pathname}`;
const currentTabHostPath = currentTabURL && `${currentTabURL.origin}${currentTabURL.pathname}`;
const tabUrlChanged = originalTabHostPath !== currentTabHostPath;
if (this.pageDetails == null || this.pageDetails.length === 0 || tabUrlChanged) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
return false;
}
try {
this.totpCode = await this.autofillService.doAutoFill({
tab: this.tab,
cipher: this.cipher,
pageDetails: this.pageDetails,
doc: window.document,
fillNewPassword: true,
allowTotpAutofill: true,
});
if (this.totpCode != null) {
this.platformUtilsService.copyToClipboard(this.totpCode, { window: window });
}
} catch {
this.platformUtilsService.showToast("error", null, this.i18nService.t("autofillError"));
this.changeDetectorRef.detectChanges();
return false;
}
return true;
}
private async handleLoadAction() {
if (!this.loadAction || this.loadAction === SHOW_AUTOFILL_BUTTON) {
return;
}
let loadActionSuccess = false;
if (this.loadAction === AUTOFILL_ID) {
loadActionSuccess = await this.fillCipher();
}
if (ViewComponent.copyActions.has(this.loadAction)) {
const { username, password } = this.cipher.login;
const copyParams: Record<CopyAction, Record<string, string>> = {
[COPY_USERNAME_ID]: { value: username, type: "username", name: "Username" },
[COPY_PASSWORD_ID]: { value: password, type: "password", name: "Password" },
[COPY_VERIFICATION_CODE_ID]: {
value: this.totpCode,
type: "verificationCodeTotp",
name: "TOTP",
},
};
const { value, type, name } = copyParams[this.loadAction as CopyAction];
loadActionSuccess = await this.copy(value, type, name);
}
if (this.inPopout) {
setTimeout(() => this.close(), loadActionSuccess ? 1000 : 0);
}
}
}

View File

@@ -7,9 +7,12 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { mockAccountServiceWith } from "@bitwarden/common/spec";
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";
@@ -32,7 +35,7 @@ describe("VaultPopupListFiltersService", () => {
} as unknown as CollectionService;
const folderService = {
folderViews$,
folderViews$: () => folderViews$,
} as unknown as FolderService;
const cipherService = {
@@ -60,6 +63,8 @@ describe("VaultPopupListFiltersService", () => {
policyAppliesToActiveUser$.next(false);
policyService.policyAppliesToActiveUser$.mockClear();
const accountService = mockAccountServiceWith("userId" as UserId);
collectionService.getAllNested = () => Promise.resolve([]);
TestBed.configureTestingModule({
providers: [
@@ -92,6 +97,10 @@ describe("VaultPopupListFiltersService", () => {
useValue: { getGlobal: () => ({ state$, update }) },
},
{ provide: FormBuilder, useClass: FormBuilder },
{
provide: AccountService,
useValue: accountService,
},
],
});

View File

@@ -20,6 +20,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -102,6 +103,8 @@ export class VaultPopupListFiltersService {
map((ciphers) => Object.values(ciphers)),
);
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private cipherService: CipherService,
@@ -111,6 +114,7 @@ export class VaultPopupListFiltersService {
private formBuilder: FormBuilder,
private policyService: PolicyService,
private stateProvider: StateProvider,
private accountService: AccountService,
) {
this.filterForm.controls.organization.valueChanges
.pipe(takeUntilDestroyed())
@@ -264,61 +268,68 @@ export class VaultPopupListFiltersService {
/**
* Folder array structured to be directly passed to `ChipSelectComponent`
*/
folders$: Observable<ChipSelectOption<FolderView>[]> = combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
folders$: Observable<ChipSelectOption<FolderView>[]> = this.activeUserId$.pipe(
switchMap((userId) =>
combineLatest([
this.filters$.pipe(
distinctUntilChanged(
(previousFilter, currentFilter) =>
// Only update the collections when the organizationId filter changes
previousFilter.organization?.id === currentFilter.organization?.id,
),
),
this.folderService.folderViews$(userId),
this.cipherViews$,
]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) {
// Do not display folder selections when only the "no folder" option is available.
return [filters, [], cipherViews];
}
// Sort folders by alphabetic name
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
let arrangedFolders = folders;
const noFolder = folders.find((f) => f.id === null);
if (noFolder) {
// Update `name` of the "no folder" option to "Items with no folder"
const updatedNoFolder = {
...noFolder,
name: this.i18nService.t("itemsWithNoFolder"),
};
// Move the "no folder" option to the end of the list
arrangedFolders = [...folders.filter((f) => f.id !== null), updatedNoFolder];
}
return [filters, arrangedFolders, cipherViews];
}),
map(([filters, folders, cipherViews]) => {
const organizationId = filters.organization?.id ?? null;
// When no org or "My vault" is selected, return all folders
if (organizationId === null || organizationId === MY_VAULT_ID) {
return folders;
}
const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId);
// Return only the folders that have ciphers within the filtered organization
return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id));
}),
map((folders) => {
const nestedFolders = this.getAllFoldersNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}),
map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
),
),
),
this.folderService.folderViews$,
this.cipherViews$,
]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) {
// Do not display folder selections when only the "no folder" option is available.
return [filters, [], cipherViews];
}
// Sort folders by alphabetic name
folders.sort(Utils.getSortFunction(this.i18nService, "name"));
let arrangedFolders = folders;
const noFolder = folders.find((f) => f.id === null);
if (noFolder) {
// Update `name` of the "no folder" option to "Items with no folder"
noFolder.name = this.i18nService.t("itemsWithNoFolder");
// Move the "no folder" option to the end of the list
arrangedFolders = [...folders.filter((f) => f.id !== null), noFolder];
}
return [filters, arrangedFolders, cipherViews];
}),
map(([filters, folders, cipherViews]) => {
const organizationId = filters.organization?.id ?? null;
// When no org or "My vault" is selected, return all folders
if (organizationId === null || organizationId === MY_VAULT_ID) {
return folders;
}
const orgCiphers = cipherViews.filter((c) => c.organizationId === organizationId);
// Return only the folders that have ciphers within the filtered organization
return folders.filter((f) => orgCiphers.some((oc) => oc.folderId === f.id));
}),
map((folders) => {
const nestedFolders = this.getAllFoldersNested(folders);
return new DynamicTreeNode<FolderView>({
fullList: folders,
nestedList: nestedFolders,
});
}),
map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
),
);
/**

View File

@@ -1,80 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "appearance" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</header>
<main tabindex="-1">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="theme">{{ "theme" | i18n }}</label>
<select
id="theme"
name="Theme"
aria-describedby="themeHelp"
[(ngModel)]="theme"
(change)="saveTheme()"
>
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="themeHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
<input
id="badge"
type="checkbox"
aria-describedby="badgeHelp"
(change)="updateBadgeCounter()"
[(ngModel)]="enableBadgeCounter"
/>
</div>
</div>
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
<input
id="favicon"
type="checkbox"
aria-describedby="faviconHelp"
(change)="updateFavicon()"
[(ngModel)]="enableFavicon"
/>
</div>
</div>
<div id="faviconHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="routing">{{ "enableAnimations" | i18n }}</label>
<input
id="routing"
type="checkbox"
(change)="updateRoutingAnimation()"
[(ngModel)]="enableRoutingAnimation"
/>
</div>
</div>
</div>
</main>

View File

@@ -1,75 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { AnimationControlService } from "@bitwarden/common/platform/abstractions/animation-control.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { enableAccountSwitching } from "../../../platform/flags";
@Component({
selector: "vault-appearance",
templateUrl: "appearance.component.html",
})
export class AppearanceComponent implements OnInit {
enableFavicon = false;
enableBadgeCounter = true;
theme: ThemeType;
themeOptions: any[];
accountSwitcherEnabled = false;
enableRoutingAnimation: boolean;
constructor(
private messagingService: MessagingService,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService,
private themeStateService: ThemeStateService,
private animationControlService: AnimationControlService,
) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
this.accountSwitcherEnabled = enableAccountSwitching();
}
async ngOnInit() {
this.enableRoutingAnimation = await firstValueFrom(
this.animationControlService.enableRoutingAnimation$,
);
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
}
async updateRoutingAnimation() {
await this.animationControlService.setEnableRoutingAnimation(this.enableRoutingAnimation);
}
async updateFavicon() {
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async saveTheme() {
await this.themeStateService.setSelectedTheme(this.theme);
}
}

View File

@@ -1,49 +0,0 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" [formGroup]="formGroup">
<header>
<div class="left">
<button type="button" routerLink="/folders">{{ "cancel" | i18n }}</button>
</div>
<h1 class="center">
<span class="title">{{ title }}</span>
</h1>
<div class="right">
<button type="submit" [disabled]="form.loading">
<span [hidden]="form.loading">{{ "save" | i18n }}</span>
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1" *ngIf="folder">
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="name">{{ "name" | i18n }}</label>
<input id="name" type="text" formControlName="name" [appAutofocus]="!editMode" />
</div>
</div>
</div>
<div class="box list" *ngIf="editMode">
<div class="box-content single-line">
<button
type="button"
class="box-content-row"
appStopClick
(click)="delete()"
[appApiAction]="deletePromise"
#deleteBtn
>
<div class="row-main text-danger">
<div class="icon text-danger" aria-hidden="true">
<i class="bwi bwi-trash bwi-lg bwi-fw" [hidden]="$any(deleteBtn).loading"></i>
<i
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
[hidden]="!$any(deleteBtn).loading"
></i>
</div>
<span>{{ "deleteFolder" | i18n }}</span>
</div>
</button>
</div>
</div>
</main>
</form>

View File

@@ -1,78 +0,0 @@
import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { first } from "rxjs/operators";
import { FolderAddEditComponent as BaseFolderAddEditComponent } from "@bitwarden/angular/vault/components/folder-add-edit.component";
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 { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@Component({
selector: "app-folder-add-edit",
templateUrl: "folder-add-edit.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class FolderAddEditComponent extends BaseFolderAddEditComponent implements OnInit {
constructor(
folderService: FolderService,
folderApiService: FolderApiServiceAbstraction,
accountService: AccountService,
keyService: KeyService,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
private router: Router,
private route: ActivatedRoute,
logService: LogService,
dialogService: DialogService,
formBuilder: FormBuilder,
) {
super(
folderService,
folderApiService,
accountService,
keyService,
i18nService,
platformUtilsService,
logService,
dialogService,
formBuilder,
);
}
async ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (params) => {
if (params.folderId) {
this.folderId = params.folderId;
}
await this.init();
});
}
async submit(): Promise<boolean> {
if (await super.submit()) {
// 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(["/folders"]);
return true;
}
return false;
}
async delete(): Promise<boolean> {
const confirmed = await super.delete();
if (confirmed) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/folders"]);
}
return confirmed;
}
}

View File

@@ -4,10 +4,13 @@ import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
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 { 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 { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { DialogService } from "@bitwarden/components";
@@ -52,8 +55,9 @@ describe("FoldersV2Component", () => {
{ provide: PlatformUtilsService, useValue: mock<PlatformUtilsService>() },
{ provide: ConfigService, useValue: mock<ConfigService>() },
{ provide: LogService, useValue: mock<LogService>() },
{ provide: FolderService, useValue: { folderViews$ } },
{ provide: FolderService, useValue: { folderViews$: () => folderViews$ } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
],
})
.overrideComponent(FoldersV2Component, {

View File

@@ -1,8 +1,10 @@
import { CommonModule } from "@angular/common";
import { Component } from "@angular/core";
import { map, Observable } from "rxjs";
import { filter, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserId } from "@bitwarden/common/types/guid";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
@@ -45,18 +47,21 @@ export class FoldersV2Component {
folders$: Observable<FolderView[]>;
NoFoldersIcon = VaultIcons.NoFolders;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor(
private folderService: FolderService,
private dialogService: DialogService,
private accountService: AccountService,
) {
this.folders$ = this.folderService.folderViews$.pipe(
this.folders$ = this.activeUserId$.pipe(
filter((userId): userId is UserId => userId !== null),
switchMap((userId) => this.folderService.folderViews$(userId)),
map((folders) => {
// Remove the last folder, which is the "no folder" option folder
if (folders.length > 0) {
return folders.slice(0, folders.length - 1);
}
return folders;
}),
);

View File

@@ -1,38 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "folders" | i18n }}</span>
</h1>
<div class="right">
<button type="button" (click)="addFolder()" appA11yTitle="{{ 'addFolder' | i18n }}">
<i class="bwi bwi-plus bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
</div>
</header>
<main tabindex="-1">
<ng-container *ngIf="folders$ | async as folders">
<div class="box list full-list" *ngIf="folders.length; else noFoldersTemplate">
<div class="box-content">
<button
type="button"
appStopClick
(click)="folderSelected(f)"
class="box-content-row padded"
*ngFor="let f of folders"
>
{{ f.name }}
</button>
</div>
</div>
</ng-container>
<ng-template #noFoldersTemplate>
<div class="no-items">
<p>{{ "noFolders" | i18n }}</p>
</div>
</ng-template>
</main>

View File

@@ -1,41 +0,0 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { map, Observable } from "rxjs";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
@Component({
selector: "app-folders",
templateUrl: "folders.component.html",
})
export class FoldersComponent {
folders$: Observable<FolderView[]>;
constructor(
private folderService: FolderService,
private router: Router,
) {
this.folders$ = this.folderService.folderViews$.pipe(
map((folders) => {
if (folders.length > 0) {
folders = folders.slice(0, folders.length - 1);
}
return folders;
}),
);
}
folderSelected(folder: FolderView) {
// 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(["/edit-folder"], { queryParams: { folderId: folder.id } });
}
addFolder() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/add-folder"]);
}
}

View File

@@ -1,35 +0,0 @@
<header>
<div class="left">
<button type="button" routerLink="/vault-settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "sync" | i18n }}</span>
</h1>
<div class="right"></div>
</header>
<main tabindex="-1">
<div class="content center-content">
<button
type="button"
class="btn block primary"
aria-describedby="lastSyncHint"
(click)="sync()"
#syncBtn
[disabled]="$any(syncBtn).loading"
[appApiAction]="syncPromise"
>
<span [hidden]="$any(syncBtn).loading">{{ "syncVaultNow" | i18n }}</span>
<i
class="bwi bwi-spinner bwi-lg bwi-spin"
[hidden]="!$any(syncBtn).loading"
aria-hidden="true"
></i>
</button>
<p id="lastSyncHint" class="text-center text-muted small">
{{ "lastSync" | i18n }} {{ lastSync }}
</p>
</div>
</main>

View File

@@ -1,46 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/platform/sync";
@Component({
selector: "app-sync",
templateUrl: "sync.component.html",
})
export class SyncComponent implements OnInit {
lastSync = "--";
syncPromise: Promise<any>;
constructor(
private syncService: SyncService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
) {}
async ngOnInit() {
await this.setLastSync();
}
async sync() {
this.syncPromise = this.syncService.fullSync(true);
const success = await this.syncPromise;
if (success) {
await this.setLastSync();
this.platformUtilsService.showToast("success", null, this.i18nService.t("syncingComplete"));
} else {
this.platformUtilsService.showToast("error", null, this.i18nService.t("syncingFailed"));
}
}
async setLastSync() {
const last = await this.syncService.getLastSync();
if (last != null) {
this.lastSync = last.toLocaleDateString() + " " + last.toLocaleTimeString();
} else {
this.lastSync = this.i18nService.t("never");
}
}
}

View File

@@ -1,56 +0,0 @@
<app-header>
<div class="left">
<button type="button" routerLink="/tabs/settings">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span>
</button>
</div>
<h1 class="center">
<span class="title">{{ "vault" | i18n }}</span>
</h1>
<div class="right">
<app-pop-out></app-pop-out>
</div>
</app-header>
<main tabindex="-1">
<div class="box list">
<div class="box-content single-line">
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/folders"
>
<div class="row-main">{{ "folders" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
appStopClick
(click)="import()"
>
<div class="row-main">{{ "importItems" | i18n }}</div>
<i
class="bwi bwi-external-link bwi-lg row-sub-icon bwi-rotate-270 bwi-fw"
aria-hidden="true"
></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/export"
>
<div class="row-main">{{ "exportVault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/sync"
>
<div class="row-main">{{ "sync" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
</div>
</div>
</main>

View File

@@ -1,25 +0,0 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
@Component({
selector: "vault-settings",
templateUrl: "vault-settings.component.html",
})
export class VaultSettingsComponent {
constructor(
public messagingService: MessagingService,
private router: Router,
) {}
async import() {
await this.router.navigate(["/import"]);
if (await BrowserApi.isPopupOpen()) {
await BrowserPopupUtils.openCurrentPagePopout(window);
}
}
}

View File

@@ -24,7 +24,7 @@ export class VaultFilterService extends BaseVaultFilterService {
collectionService: CollectionService,
policyService: PolicyService,
stateProvider: StateProvider,
private accountService: AccountService,
accountService: AccountService,
) {
super(
organizationService,
@@ -33,6 +33,7 @@ export class VaultFilterService extends BaseVaultFilterService {
collectionService,
policyService,
stateProvider,
accountService,
);
this.vaultFilter.myVaultOnly = false;
this.vaultFilter.selectedOrganizationId = null;

View File

@@ -118,58 +118,58 @@
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Name" xml:space="preserve">
<value>Bitwarden - wachtwoordbeheerder</value>
<value>Bitwarden Wachtwoordbeheerder</value>
</data>
<data name="Summary" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie.</value>
</data>
<data name="Description" xml:space="preserve">
<value>Recognized as the best password manager by PCMag, WIRED, The Verge, CNET, G2, and more!
<value>Erkend als de beste wachtwoordmanager door PCMag, WIRED, The Verge, CNET, G2 en meer!
SECURE YOUR DIGITAL LIFE
Secure your digital life and protect against data breaches by generating and saving unique, strong passwords for every account. Maintain everything in an end-to-end encrypted password vault that only you can access.
BEVEILIG JE DIGITALE LEVEN
Beveilig je digitale leven en bescherm je tegen datalekken door unieke, sterke wachtwoorden te genereren en op te slaan voor elke account. Bewaar alles in een end-to-end versleutelde wachtwoordkluis waar alleen jij toegang toe hebt.
ACCESS YOUR DATA, ANYWHERE, ANYTIME, ON ANY DEVICE
Easily manage, store, secure, and share unlimited passwords across unlimited devices without restrictions.
OVERAL EN ALTIJD TOEGANG TOT JE GEGEVENS, OP ELK APPARAAT
Beheer, bewaar, beveilig en deel een onbeperkt aantal wachtwoorden op een onbeperkt aantal apparaten zonder beperkingen.
EVERYONE SHOULD HAVE THE TOOLS TO STAY SAFE ONLINE
Utilize Bitwarden for free with no ads or selling data. Bitwarden believes everyone should have the ability to stay safe online. Premium plans offer access to advanced features.
IEDEREEN ZOU DE MIDDELEN MOETEN HEBBEN OM VEILIG ONLINE TE BLIJVEN
Gebruik Bitwarden gratis, zonder advertenties of verkoop van gegevens. Bitwarden vindt dat iedereen de mogelijkheid moet hebben om veilig online te zijn. Premium abonnementen bieden toegang tot geavanceerde functies.
EMPOWER YOUR TEAMS WITH BITWARDEN
Plans for Teams and Enterprise come with professional business features. Some examples include SSO integration, self-hosting, directory integration and SCIM provisioning, global policies, API access, event logs, and more.
VERSTERK JE TEAMS MET BITWARDEN
Abonnementen voor Teams en Enterprise worden geleverd met professionele zakelijke functies. Enkele voorbeelden zijn SSO-integratie, zelf hosten, directory-integratie en SCIM provisioning, globaal beleid, API-toegang, gebeurtenislogboeken en meer.
Use Bitwarden to secure your workforce and share sensitive information with colleagues.
Gebruik Bitwarden om je medewerkers te beveiligen en gevoelige informatie te delen met collega's.
More reasons to choose Bitwarden:
Meer redenen om voor Bitwarden te kiezen:
World-Class Encryption
Passwords are protected with advanced end-to-end encryption (AES-256 bit, salted hashtag, and PBKDF2 SHA-256) so your data stays secure and private.
Encryptie van wereldklasse
Wachtwoorden worden beschermd met geavanceerde end-to-end versleuteling (AES-256 bit, salted hashtag en PBKDF2 SHA-256) zodat je gegevens veilig en privé blijven.
3rd-party Audits
Bitwarden regularly conducts comprehensive third-party security audits with notable security firms. These annual audits include source code assessments and penetration testing across Bitwarden IPs, servers, and web applications.
Audits door derde partijen
Bitwarden voert regelmatig uitgebreide beveiligingsaudits uit bij gerenommeerde beveiligingsbedrijven. Deze jaarlijkse audits omvatten broncodebeoordelingen en penetratietests voor Bitwarden IP's, servers en webapplicaties.
Advanced 2FA
Secure your login with a third-party authenticator, emailed codes, or FIDO2 WebAuthn credentials such as a hardware security key or passkey.
Geavanceerde 2FA
Beveilig je login met een authenticator van derden, codes per e-mail of FIDO2 WebAuthn referenties zoals een hardware beveiligingssleutel of passkey.
Bitwarden Send
Transmit data directly to others while maintaining end-to-end encrypted security and limiting exposure.
Verstuur gegevens rechtstreeks naar anderen met behoud van end-to-end versleutelde beveiliging en beperking van blootstelling.
Built-in Generator
Create long, complex, and distinct passwords and unique usernames for every site you visit. Integrate with email alias providers for additional privacy.
Ingebouwde generator
Maak lange, complexe en duidelijke wachtwoorden en unieke gebruikersnamen voor elke site die je bezoekt. Integreer met e-mail alias providers voor extra privacy.
Global Translations
Bitwarden translations exist for more than 60 languages, translated by the global community though Crowdin.
Wereldwijde vertalingen
Bitwarden vertalingen bestaan voor meer dan 60 talen, vertaald door de wereldwijde gemeenschap via Crowdin.
Cross-Platform Applications
Secure and share sensitive data within your Bitwarden Vault from any browser, mobile device, or desktop OS, and more.
Platformoverkoepelende applicaties
Beveilig en deel gevoelige gegevens in je Bitwarden Vault vanuit elke browser, mobiel apparaat of desktop OS, en meer.
Bitwarden secures more than just passwords
End-to-end encrypted credential management solutions from Bitwarden empower organizations to secure everything, including developer secrets and passkey experiences. Visit Bitwarden.com to learn more about Bitwarden Secrets Manager and Bitwarden Passwordless.dev!
Bitwarden beveiligt meer dan alleen wachtwoorden
Met de end-to-end versleutelde oplossingen voor referentiebeheer van Bitwarden kunnen organisaties alles beveiligen, inclusief ontwikkelaarsgeheimen en ervaringen met wachtwoorden. Bezoek Bitwarden.com voor meer informatie over Bitwarden Secrets Manager en Bitwarden Passwordless.dev!
</value>
</data>
<data name="AssetTitle" xml:space="preserve">
<value>At home, at work, or on the go, Bitwarden easily secures all your passwords, passkeys, and sensitive information.</value>
<value>Thuis, op het werk of onderweg, Bitwarden beveiligt eenvoudig al je wachtwoorden, sleutels en gevoelige informatie.</value>
</data>
<data name="ScreenshotSync" xml:space="preserve">
<value>Synchroniseer en gebruik je kluis op meerdere apparaten</value>

View File

@@ -24,6 +24,8 @@ 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,
@@ -121,12 +123,12 @@ export class EditCommand {
cipher.collectionIds = req;
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher);
const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher,
await firstValueFrom(this.activeUserId$),
),
);
const res = new CipherResponse(decCipher);
return Response.success(res);
@@ -136,7 +138,8 @@ export class EditCommand {
}
private async editFolder(id: string, req: FolderExport) {
const folder = await this.folderService.getFromState(id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
}
@@ -144,12 +147,11 @@ export class EditCommand {
let folderView = await folder.decrypt();
folderView = FolderExport.toView(req, folderView);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const encFolder = await this.folderService.encrypt(folderView, userKey);
try {
await this.folderApiService.save(encFolder);
const updatedFolder = await this.folderService.get(folder.id);
await this.folderApiService.save(encFolder, activeUserId);
const updatedFolder = await this.folderService.get(folder.id, activeUserId);
const decFolder = await updatedFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);

View File

@@ -51,6 +51,8 @@ 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,
@@ -113,10 +115,8 @@ export class GetCommand extends DownloadCommand {
let decCipher: CipherView = null;
if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (cipher != null) {
const activeUserId = await firstValueFrom(this.activeUserId$);
decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
);
@@ -383,13 +383,14 @@ export class GetCommand extends DownloadCommand {
private async getFolder(id: string) {
let decFolder: FolderView = null;
const activeUserId = await firstValueFrom(this.activeUserId$);
if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) {
decFolder = await folder.decrypt();
}
} else if (id.trim() !== "") {
let folders = await this.folderService.getAllDecryptedFromState();
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
folders = CliUtils.searchFolders(folders, id);
if (folders.length > 1) {
return Response.multipleResults(folders.map((f) => f.id));
@@ -551,9 +552,7 @@ export class GetCommand extends DownloadCommand {
private async getFingerprint(id: string) {
let fingerprint: string[] = null;
if (id === "me") {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
} else if (Utils.isGuid(id)) {

View File

@@ -1,4 +1,4 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import {
OrganizationUserApiService,
@@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { EventType } from "@bitwarden/common/enums";
import { ListResponse as ApiListResponse } from "@bitwarden/common/models/response/list.response";
import { Utils } from "@bitwarden/common/platform/misc/utils";
@@ -38,6 +39,7 @@ export class ListCommand {
private organizationUserApiService: OrganizationUserApiService,
private apiService: ApiService,
private eventCollectionService: EventCollectionService,
private accountService: AccountService,
) {}
async run(object: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -135,7 +137,10 @@ export class ListCommand {
}
private async listFolders(options: Options) {
let folders = await this.folderService.getAllDecryptedFromState();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
if (options.search != null && options.search.trim() !== "") {
folders = CliUtils.searchFolders(folders, options.search);

View File

@@ -76,6 +76,7 @@ export class OssServeConfigurator {
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService,
);
this.createCommand = new CreateCommand(
this.serviceContainer.cipherService,
@@ -115,6 +116,7 @@ export class OssServeConfigurator {
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService,
);
this.confirmCommand = new ConfirmCommand(
this.serviceContainer.apiService,

View File

@@ -113,6 +113,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.organizationUserApiService,
this.serviceContainer.apiService,
this.serviceContainer.eventCollectionService,
this.serviceContainer.accountService,
);
const response = await command.run(object, cmd);
@@ -321,6 +322,7 @@ export class VaultProgram extends BaseProgram {
this.serviceContainer.folderApiService,
this.serviceContainer.billingAccountProfileStateService,
this.serviceContainer.cipherAuthorizationService,
this.serviceContainer.accountService,
);
const response = await command.run(object, id, cmd);
this.processResponse(response);

View File

@@ -30,6 +30,8 @@ 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,
@@ -86,9 +88,7 @@ export class CreateCommand {
}
private async createCipher(req: CipherExport) {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try {
const newCipher = await this.cipherService.createWithServer(cipher);
@@ -152,9 +152,7 @@ export class CreateCommand {
}
try {
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const activeUserId = await firstValueFrom(this.activeUserId$);
const updatedCipher = await this.cipherService.saveAttachmentRawWithServer(
cipher,
fileName,
@@ -171,12 +169,12 @@ export class CreateCommand {
}
private async createFolder(req: FolderExport) {
const activeAccountId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeAccountId.id);
const activeUserId = await firstValueFrom(this.activeUserId$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try {
await this.folderApiService.save(folder);
const newFolder = await this.folderService.get(folder.id);
await this.folderApiService.save(folder, activeUserId);
const newFolder = await this.folderService.get(folder.id, activeUserId);
const decFolder = await newFolder.decrypt();
const res = new FolderResponse(decFolder);
return Response.success(res);

View File

@@ -1,6 +1,7 @@
import { firstValueFrom } from "rxjs";
import { firstValueFrom, map } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -19,6 +20,7 @@ export class DeleteCommand {
private folderApiService: FolderApiServiceAbstraction,
private accountProfileService: BillingAccountProfileStateService,
private cipherAuthorizationService: CipherAuthorizationService,
private accountService: AccountService,
) {}
async run(object: string, id: string, cmdOptions: Record<string, any>): Promise<Response> {
@@ -103,13 +105,16 @@ export class DeleteCommand {
}
private async deleteFolder(id: string) {
const folder = await this.folderService.getFromState(id);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) {
return Response.notFound();
}
try {
await this.folderApiService.delete(id);
await this.folderApiService.delete(id, activeUserId);
return Response.success();
} catch (e) {
return Response.error(e);

View File

@@ -567,9 +567,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.2"
version = "1.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f34d93e62b03caf570cccc334cbc6c2fceca82f39211051345108adcba3eebdc"
checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333"
dependencies = [
"shlex",
]
@@ -616,9 +616,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69371e34337c4c984bbe322360c2547210bf632eb2814bbe78a6e87a2935bd2b"
checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
dependencies = [
"clap_builder",
"clap_derive",
@@ -626,9 +626,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.22"
version = "4.5.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24c1b4099818523236a8ca881d2b45db98dadfb4625cf6608c12069fcbbde1"
checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
dependencies = [
"anstream",
"anstyle",
@@ -650,9 +650,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clipboard-win"
@@ -703,16 +703,6 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.0"
@@ -740,9 +730,9 @@ dependencies = [
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@@ -759,9 +749,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crypto-common"
@@ -821,9 +811,9 @@ dependencies = [
[[package]]
name = "cxx"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef"
checksum = "4d44ff199ff93242c3afe480ab588d544dd08d72e92885e152ffebc670f076ad"
dependencies = [
"cc",
"cxxbridge-cmd",
@@ -835,9 +825,9 @@ dependencies = [
[[package]]
name = "cxx-build"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afa390d956ee7ccb41aeed7ed7856ab3ffb4fc587e7216be7e0f83e949b4e6c"
checksum = "66fd8f17ad454fc1e4f4ab83abffcc88a532e90350d3ffddcb73030220fcbd52"
dependencies = [
"cc",
"codespan-reporting",
@@ -849,9 +839,9 @@ dependencies = [
[[package]]
name = "cxxbridge-cmd"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6"
checksum = "4717c9c806a9e07fdcb34c84965a414ea40fafe57667187052cf1eb7f5e8a8a9"
dependencies = [
"clap",
"codespan-reporting",
@@ -862,15 +852,15 @@ dependencies = [
[[package]]
name = "cxxbridge-flags"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa"
checksum = "2f6515329bf3d98f4073101c7866ff2bec4e635a13acb82e3f3753fff0bf43cb"
[[package]]
name = "cxxbridge-macro"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95"
checksum = "fb93e6a7ce8ec985c02bbb758237a31598b340acbbc3c19c5a4fa6adaaac92ab"
dependencies = [
"proc-macro2",
"quote",
@@ -935,7 +925,7 @@ dependencies = [
"bitwarden-russh",
"byteorder",
"cbc",
"core-foundation 0.10.0",
"core-foundation",
"desktop_objc",
"dirs",
"ed25519",
@@ -996,7 +986,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"cc",
"core-foundation 0.9.4",
"core-foundation",
"glob",
"thiserror",
"tokio",
@@ -1144,7 +1134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1176,9 +1166,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
@@ -1518,9 +1508,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.162"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libloading"
@@ -1529,7 +1519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -1643,9 +1633,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.0"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
dependencies = [
"adler2",
]
@@ -1677,9 +1667,9 @@ dependencies = [
[[package]]
name = "napi-build"
version = "2.1.3"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c0f5d67ee408a4685b61f5ab7e58605c8ae3f2b4189f0127d804ff13d5560a"
checksum = "db836caddef23662b94e16bf1f26c40eceb09d6aee5d5b06a7ac199320b69b19"
[[package]]
name = "napi-derive"
@@ -2375,9 +2365,9 @@ checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175"
[[package]]
name = "redox_syscall"
version = "0.5.7"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
dependencies = [
"bitflags",
]
@@ -2479,15 +2469,15 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.41"
version = "0.38.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6"
checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
dependencies = [
"bitflags",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2562,12 +2552,12 @@ dependencies = [
[[package]]
name = "security-framework"
version = "3.0.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d0283c0a4a22a0f1b0e4edca251aa20b92fc96eaa09b84bec052f9415e9d71"
checksum = "81d3f8c9bfcc3cbb6b0179eb57042d75b1582bdc65c3cb95f3fa999509c03cbc"
dependencies = [
"bitflags",
"core-foundation 0.10.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
@@ -2575,9 +2565,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.12.0"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
dependencies = [
"core-foundation-sys",
"libc",
@@ -2585,27 +2575,27 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.23"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
dependencies = [
"serde",
]
[[package]]
name = "serde"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
@@ -2846,7 +2836,7 @@ dependencies = [
"fastrand",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3350,7 +3340,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]

View File

@@ -31,7 +31,7 @@ base64 = "=0.22.1"
byteorder = "=1.5.0"
cbc = { version = "=0.1.2", features = ["alloc"] }
homedir = "=0.3.4"
libc = "=0.2.162"
libc = "=0.2.169"
pin-project = "=1.1.7"
dirs = "=5.0.1"
futures = "=0.3.31"
@@ -81,8 +81,8 @@ keytar = "=0.1.6"
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { version = "=0.10.0", optional = true }
security-framework = { version = "=3.0.0", optional = true }
security-framework-sys = { version = "=2.12.0", optional = true }
security-framework = { version = "=3.1.0", optional = true }
security-framework-sys = { version = "=2.13.0", optional = true }
desktop_objc = { path = "../objc" }
[target.'cfg(target_os = "linux")'.dependencies]

View File

@@ -21,10 +21,10 @@ serde = { version = "1.0.205", features = ["derive"] }
serde_json = "1.0.122"
tokio = { version = "1.39.2", features = ["sync"] }
tokio-util = "0.7.11"
uniffi = { version = "0.28.0", features = ["cli"] }
uniffi = { version = "0.28.3", features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
oslog = "0.2.0"
[build-dependencies]
uniffi = { version = "0.28.0", features = ["build"] }
uniffi = { version = "0.28.3", features = ["build"] }

Some files were not shown because too many files have changed in this diff Show More