mirror of
https://github.com/bitwarden/browser
synced 2026-02-05 19:23:19 +00:00
merge main, fix conflict
This commit is contained in:
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
@@ -157,6 +157,7 @@
|
||||
"html-webpack-injector",
|
||||
"html-webpack-plugin",
|
||||
"interprocess",
|
||||
"itertools",
|
||||
"json5",
|
||||
"keytar",
|
||||
"libc",
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/auto-branch-updater.yml
vendored
2
.github/workflows/auto-branch-updater.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: 'eu-web-${{ steps.setup.outputs.branch }}'
|
||||
fetch-depth: 0
|
||||
|
||||
12
.github/workflows/build-browser.yml
vendored
12
.github/workflows/build-browser.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
working-directory: apps/browser
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -254,7 +254,7 @@ jobs:
|
||||
artifact_name: "dist-opera-MV3"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -542,7 +542,7 @@ jobs:
|
||||
- build-safari
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/build-cli.yml
vendored
8
.github/workflows/build-cli.yml
vendored
@@ -59,7 +59,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -311,7 +311,7 @@ jobs:
|
||||
_WIN_PKG_VERSION: 3.5
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -520,7 +520,7 @@ jobs:
|
||||
_PACKAGE_VERSION: ${{ needs.setup.outputs.package_version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
20
.github/workflows/build-desktop.yml
vendored
20
.github/workflows/build-desktop.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: true
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
@@ -343,7 +343,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -491,7 +491,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
NODE_OPTIONS: --max_old_space_size=4096
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1004,7 +1004,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1244,7 +1244,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1519,7 +1519,7 @@ jobs:
|
||||
working-directory: apps/desktop
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -1860,7 +1860,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/build-web.yml
vendored
8
.github/workflows/build-web.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
_VERSION: ${{ needs.setup.outputs.version }}
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
@@ -174,7 +174,7 @@ jobs:
|
||||
echo "server_ref=$SERVER_REF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out Server repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
path: server
|
||||
repository: bitwarden/server
|
||||
@@ -367,7 +367,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/chromatic.yml
vendored
2
.github/workflows/chromatic.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
2
.github/workflows/crowdin-pull.yml
vendored
2
.github/workflows/crowdin-pull.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
permission-pull-requests: write # for generating pull requests
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
persist-credentials: false
|
||||
|
||||
2
.github/workflows/lint-crowdin-config.yml
vendored
2
.github/workflows/lint-crowdin-config.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
]
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/locales-lint.yml
vendored
4
.github/workflows/locales-lint.yml
vendored
@@ -17,11 +17,11 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Checkout base branch repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: base
|
||||
|
||||
2
.github/workflows/nx.yml
vendored
2
.github/workflows/nx.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
6
.github/workflows/publish-cli.yml
vendored
6
.github/workflows/publish-cli.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -151,7 +151,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -203,7 +203,7 @@ jobs:
|
||||
_PKG_VERSION: ${{ needs.setup.outputs.release_version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
6
.github/workflows/publish-desktop.yml
vendored
6
.github/workflows/publish-desktop.yml
vendored
@@ -204,7 +204,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
_RELEASE_TAG: ${{ needs.setup.outputs.tag_name }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/publish-web.yml
vendored
4
.github/workflows/publish-web.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
echo "Github Release Option: $_RELEASE_OPTION"
|
||||
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/release-browser.yml
vendored
4
.github/workflows/release-browser.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-cli.yml
vendored
2
.github/workflows/release-cli.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
release_version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-desktop.yml
vendored
2
.github/workflows/release-desktop.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
||||
release_channel: ${{ steps.release_channel.outputs.channel }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/release-web.yml
vendored
2
.github/workflows/release-web.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
tag_version: ${{ steps.version.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
4
.github/workflows/repository-management.yml
vendored
4
.github/workflows/repository-management.yml
vendored
@@ -105,7 +105,7 @@ jobs:
|
||||
permission-contents: write # for committing and pushing to current branch
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
@@ -471,7 +471,7 @@ jobs:
|
||||
permission-contents: write # for creating and pushing new branch
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Check out clients repository
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
8
.github/workflows/test.yml
vendored
8
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
sudo apt-get install -y gnome-keyring dbus-x11
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
- rust-coverage
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
2
.github/workflows/version-auto-bump.yml
vendored
2
.github/workflows/version-auto-bump.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
permission-contents: write # for committing and pushing to the current branch
|
||||
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/browser",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"scripts": {
|
||||
"build": "npm run build:chrome",
|
||||
"build:bit": "npm run build:bit:chrome",
|
||||
|
||||
@@ -2498,7 +2498,7 @@
|
||||
}
|
||||
},
|
||||
"topLayerHijackWarning": {
|
||||
"message": "This page is interfering with the Bitwarden experience. The Bitwarden inline menu has been temporarily disabled as a safety measure."
|
||||
"message": "Bu səhifə Bitwarden təcrübəsinə müdaxilə edir. Bitwarden daxili menyusu, təhlükəsizlik tədbiri olaraq müvəqqəti sıradan çıxarılıb."
|
||||
},
|
||||
"setMasterPassword": {
|
||||
"message": "Ana parolu ayarla"
|
||||
@@ -4124,7 +4124,7 @@
|
||||
"message": "Avto-doldurula bilmir"
|
||||
},
|
||||
"cannotAutofillExactMatch": {
|
||||
"message": "Default matching is set to 'Exact Match'. The current website does not exactly match the saved login details for this item."
|
||||
"message": "İlkin uyuşma 'Tam Uyuşur' olaraq ayarlanıb. Hazırkı veb sayt, bu element üçün saxlanılmış giriş məlumatları ilə tam uyuşmur."
|
||||
},
|
||||
"okay": {
|
||||
"message": "Oldu"
|
||||
|
||||
@@ -6039,5 +6039,8 @@
|
||||
},
|
||||
"whyAmISeeingThis": {
|
||||
"message": "Why am I seeing this?"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,7 +1486,7 @@
|
||||
"message": "选择一个文件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"message": "文件最大为 500 MB。"
|
||||
@@ -3804,7 +3804,7 @@
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "桌面",
|
||||
"message": "桌面端",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"webVault": {
|
||||
@@ -5707,7 +5707,7 @@
|
||||
"message": "导入现有密码"
|
||||
},
|
||||
"emptyVaultNudgeBody": {
|
||||
"message": "使用导入器快速将登录传输到 Bitwarden 而无需手动添加。"
|
||||
"message": "使用导入器快速将登录转移到 Bitwarden 而无需手动添加。"
|
||||
},
|
||||
"emptyVaultNudgeButton": {
|
||||
"message": "立即导入"
|
||||
@@ -6014,7 +6014,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6023,7 +6023,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -6032,7 +6032,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { EVENTS } from "@bitwarden/common/autofill/constants";
|
||||
|
||||
import { BrowserApi } from "../../../../platform/browser/browser-api";
|
||||
@@ -84,11 +82,15 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
}
|
||||
const { type, typeData, params } = message.data;
|
||||
|
||||
if (!typeData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.currentNotificationBarType && type !== this.currentNotificationBarType) {
|
||||
this.closeNotificationBar();
|
||||
}
|
||||
|
||||
const initData = {
|
||||
const initData: NotificationBarIframeInitData = {
|
||||
type: type as NotificationType,
|
||||
isVaultLocked: typeData.isVaultLocked,
|
||||
theme: typeData.theme,
|
||||
@@ -116,7 +118,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const closedByUser =
|
||||
typeof message.data?.closedByUser === "boolean" ? message.data.closedByUser : true;
|
||||
if (message.data?.fadeOutNotification) {
|
||||
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
|
||||
if (this.notificationBarIframeElement) {
|
||||
setElementStyles(this.notificationBarIframeElement, { opacity: "0" }, true);
|
||||
}
|
||||
globalThis.setTimeout(() => this.closeNotificationBar(closedByUser), 150);
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +170,9 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
this.createNotificationBarElement();
|
||||
|
||||
this.setupInitNotificationBarMessageListener(initData);
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
if (this.notificationBarRootElement) {
|
||||
globalThis.document.body.appendChild(this.notificationBarRootElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +185,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const isNotificationFresh =
|
||||
initData.launchTimestamp && Date.now() - initData.launchTimestamp < 250;
|
||||
|
||||
this.currentNotificationBarType = initData.type;
|
||||
this.currentNotificationBarType = initData.type ?? null;
|
||||
this.notificationBarIframeElement = globalThis.document.createElement("iframe");
|
||||
this.notificationBarIframeElement.id = "bit-notification-bar-iframe";
|
||||
const parentOrigin = globalThis.location.origin;
|
||||
@@ -206,11 +212,13 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
* This will animate the notification bar into view.
|
||||
*/
|
||||
private handleNotificationBarIframeOnLoad = () => {
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
if (this.notificationBarIframeElement) {
|
||||
setElementStyles(
|
||||
this.notificationBarIframeElement,
|
||||
{ transform: "translateX(0)", opacity: "1" },
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
this.notificationBarIframeElement?.removeEventListener(
|
||||
EVENTS.LOAD,
|
||||
@@ -252,6 +260,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
const handleInitNotificationBarMessage = (event: MessageEvent) => {
|
||||
const { source, data } = event;
|
||||
if (
|
||||
!this.notificationBarIframeElement?.contentWindow ||
|
||||
source !== this.notificationBarIframeElement.contentWindow ||
|
||||
data?.command !== "initNotificationBar"
|
||||
) {
|
||||
@@ -282,13 +291,14 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
return;
|
||||
}
|
||||
|
||||
this.notificationBarIframeElement.remove();
|
||||
this.notificationBarIframeElement?.remove();
|
||||
this.notificationBarIframeElement = null;
|
||||
|
||||
this.notificationBarElement.remove();
|
||||
this.notificationBarElement?.remove();
|
||||
this.notificationBarElement = null;
|
||||
this.notificationBarShadowRoot = null;
|
||||
this.notificationBarRootElement.remove();
|
||||
|
||||
this.notificationBarRootElement?.remove();
|
||||
this.notificationBarRootElement = null;
|
||||
|
||||
const removableNotificationTypes = new Set([
|
||||
@@ -297,7 +307,11 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
NotificationTypes.AtRiskPassword,
|
||||
] as NotificationType[]);
|
||||
|
||||
if (closedByUserAction && removableNotificationTypes.has(this.currentNotificationBarType)) {
|
||||
if (
|
||||
closedByUserAction &&
|
||||
this.currentNotificationBarType &&
|
||||
removableNotificationTypes.has(this.currentNotificationBarType)
|
||||
) {
|
||||
void sendExtensionMessage("bgRemoveTabFromNotificationQueue");
|
||||
}
|
||||
|
||||
@@ -310,7 +324,7 @@ export class OverlayNotificationsContentService implements OverlayNotificationsC
|
||||
* @param message - The message to send to the notification bar iframe.
|
||||
*/
|
||||
private sendMessageToNotificationBarIframe(message: Record<string, any>) {
|
||||
if (this.notificationBarIframeElement) {
|
||||
if (this.notificationBarIframeElement?.contentWindow) {
|
||||
this.notificationBarIframeElement.contentWindow.postMessage(message, this.extensionOrigin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 2,
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"minimum_chrome_version": "102.0",
|
||||
"name": "__MSG_extName__",
|
||||
"short_name": "Bitwarden",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"description": "__MSG_extDesc__",
|
||||
"default_locale": "en",
|
||||
"author": "Bitwarden Inc.",
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015-2025 Bitwarden Inc. All rights reserved.</string>
|
||||
<string>Copyright © 2015-2026 Bitwarden Inc. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<string>$(PRODUCT_MODULE_NAME).SafariWebExtensionHandler</string>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2015-2025 Bitwarden Inc. All rights reserved.</string>
|
||||
<string>Copyright © 2015-2026 Bitwarden Inc. All rights reserved.</string>
|
||||
<key>NSHumanReadableDescription</key>
|
||||
<string>A secure and free password manager for all of your devices.</string>
|
||||
<key>SFSafariAppExtensionBundleIdentifiersToReplace</key>
|
||||
|
||||
@@ -1,39 +1,37 @@
|
||||
<main class="tw-top-0">
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
<popup-page>
|
||||
<popup-header
|
||||
slot="header"
|
||||
[pageTitle]="'createdSend' | i18n"
|
||||
showBackButton
|
||||
[backAction]="goToEditSend.bind(this)"
|
||||
>
|
||||
<ng-container slot="end">
|
||||
<app-pop-out></app-pop-out>
|
||||
</ng-container>
|
||||
</popup-header>
|
||||
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<div
|
||||
class="tw-flex tw-bg-background-alt tw-flex-col tw-justify-center tw-items-center tw-gap-2 tw-h-full tw-px-5"
|
||||
>
|
||||
<div class="tw-size-[95px] tw-content-center">
|
||||
<bit-icon [icon]="sendCreatedIcon"></bit-icon>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
</main>
|
||||
<h3 tabindex="0" appAutofocus class="tw-font-medium">
|
||||
{{ "createdSendSuccessfully" | i18n }}
|
||||
</h3>
|
||||
<p class="tw-text-center">
|
||||
{{ formatExpirationDate() }}
|
||||
</p>
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
</div>
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary" (click)="copyLink()">
|
||||
<b>{{ "copyLink" | i18n }}</b>
|
||||
</button>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="goBack()">
|
||||
{{ "close" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
</popup-page>
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
}
|
||||
@if (savedUrls().length > 1) {
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mt-4 tw-mb-1 tw-pt-2">
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length }}
|
||||
<p class="tw-text-muted tw-text-xs tw-uppercase tw-font-medium tw-mb-0">
|
||||
{{ "savedWebsites" | i18n: savedUrls().length.toString() }}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,17 +13,6 @@
|
||||
<button type="button" bitMenuItem (click)="doAutofill()">
|
||||
{{ "autofill" | i18n }}
|
||||
</button>
|
||||
<!-- Autofill confirmation handles both 'autofill' and 'autofill and save' so no need to show both -->
|
||||
@if (!(autofillConfirmationFlagEnabled$ | async)) {
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
*ngIf="canEdit && isLogin"
|
||||
(click)="doAutofillAndSave()"
|
||||
>
|
||||
{{ "fillAndSave" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="showViewOption">
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
UriMatchStrategy,
|
||||
UriMatchStrategySetting,
|
||||
} from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -40,10 +39,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
openSimpleDialog: jest.fn().mockResolvedValue(true),
|
||||
open: jest.fn(),
|
||||
};
|
||||
const featureFlag$ = new BehaviorSubject<boolean>(false);
|
||||
const configService = {
|
||||
getFeatureFlag$: jest.fn().mockImplementation(() => featureFlag$.asObservable()),
|
||||
};
|
||||
const cipherService = {
|
||||
getFullCipherView: jest.fn(),
|
||||
encrypt: jest.fn(),
|
||||
@@ -93,7 +88,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [ItemMoreOptionsComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
{ provide: CipherService, useValue: cipherService },
|
||||
{ provide: VaultPopupAutofillService, useValue: autofillSvc },
|
||||
|
||||
@@ -152,22 +146,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("calls the autofill service to autofill without showing the confirmation dialog when the feature flag is disabled", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
|
||||
await component.doAutofill();
|
||||
|
||||
expect(cipherService.getFullCipherView).toHaveBeenCalled();
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledTimes(1);
|
||||
expect(autofillSvc.doAutofill).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: "cipher-1" }),
|
||||
true,
|
||||
true,
|
||||
);
|
||||
expect(autofillSvc.doAutofillAndSave).not.toHaveBeenCalled();
|
||||
expect(dialogService.openSimpleDialog).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does nothing if the user fails master password reprompt", async () => {
|
||||
baseCipher.reprompt = 2; // Master Password reprompt enabled
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com" });
|
||||
@@ -181,7 +159,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
});
|
||||
|
||||
it("does not show the exact match dialog when the default match strategy is Exact and autofill confirmation is not to be shown", async () => {
|
||||
// autofill confirmation dialog is not shown when either the feature flag is disabled
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Exact);
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
await component.doAutofill();
|
||||
@@ -191,8 +168,6 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
|
||||
describe("autofill confirmation dialog", () => {
|
||||
beforeEach(() => {
|
||||
// autofill confirmation dialog is shown when feature flag is enabled
|
||||
featureFlag$.next(true);
|
||||
uriMatchStrategy$.next(UriMatchStrategy.Domain);
|
||||
passwordRepromptService.passwordRepromptCheck.mockResolvedValue(true);
|
||||
});
|
||||
@@ -206,7 +181,7 @@ describe("ItemMoreOptionsComponent", () => {
|
||||
expect(passwordRepromptService.passwordRepromptCheck).toHaveBeenCalledWith(baseCipher);
|
||||
});
|
||||
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs when the feature flag is enabled", async () => {
|
||||
it("opens the autofill confirmation dialog with filtered saved URLs", async () => {
|
||||
autofillSvc.currentAutofillTab$.next({ url: "https://page.example.com/path" });
|
||||
const openSpy = mockConfirmDialogResult(AutofillConfirmationDialogResult.Canceled);
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
@@ -37,7 +35,6 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
|
||||
import { BrowserPremiumUpgradePromptService } from "../../../services/browser-premium-upgrade-prompt.service";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { VaultPopupItemsService } from "../../../services/vault-popup-items.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
import {
|
||||
AutofillConfirmationDialogComponent,
|
||||
@@ -98,10 +95,6 @@ export class ItemMoreOptionsComponent {
|
||||
|
||||
protected autofillAllowed$ = this.vaultPopupAutofillService.autofillAllowed$;
|
||||
|
||||
protected autofillConfirmationFlagEnabled$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.AutofillConfirmation)
|
||||
.pipe(map((isFeatureFlagEnabled) => isFeatureFlagEnabled));
|
||||
|
||||
protected uriMatchStrategy$ = this.domainSettingsService.resolvedDefaultUriMatchStrategy$;
|
||||
|
||||
/**
|
||||
@@ -166,8 +159,6 @@ export class ItemMoreOptionsComponent {
|
||||
private collectionService: CollectionService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private cipherArchiveService: CipherArchiveService,
|
||||
private configService: ConfigService,
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
) {}
|
||||
|
||||
@@ -216,13 +207,9 @@ export class ItemMoreOptionsComponent {
|
||||
const cipherHasAllExactMatchLoginUris =
|
||||
uris.length > 0 && uris.every((u) => u.uri && u.match === UriMatchStrategy.Exact);
|
||||
|
||||
const showAutofillConfirmation = await firstValueFrom(this.autofillConfirmationFlagEnabled$);
|
||||
const uriMatchStrategy = await firstValueFrom(this.uriMatchStrategy$);
|
||||
|
||||
if (
|
||||
showAutofillConfirmation &&
|
||||
(cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact)
|
||||
) {
|
||||
if (cipherHasAllExactMatchLoginUris || uriMatchStrategy === UriMatchStrategy.Exact) {
|
||||
await this.dialogService.openSimpleDialog({
|
||||
title: { key: "cannotAutofill" },
|
||||
content: { key: "cannotAutofillExactMatch" },
|
||||
@@ -233,11 +220,6 @@ export class ItemMoreOptionsComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showAutofillConfirmation) {
|
||||
await this.vaultPopupAutofillService.doAutofill(cipher, true, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTab = await firstValueFrom(this.vaultPopupAutofillService.currentAutofillTab$);
|
||||
|
||||
if (!currentTab?.url) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/cli",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.12.0",
|
||||
"version": "2025.12.1",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<authors>Bitwarden Inc.</authors>
|
||||
<projectUrl>https://bitwarden.com/</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png</iconUrl>
|
||||
<copyright>Copyright © 2015-2025 Bitwarden Inc.</copyright>
|
||||
<copyright>Copyright © 2015-2026 Bitwarden Inc.</copyright>
|
||||
<projectSourceUrl>https://github.com/bitwarden/clients/</projectSourceUrl>
|
||||
<docsUrl>https://help.bitwarden.com/article/cli/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/bitwarden/clients/issues</bugTrackerUrl>
|
||||
|
||||
16
apps/desktop/desktop_native/Cargo.lock
generated
16
apps/desktop/desktop_native/Cargo.lock
generated
@@ -329,6 +329,7 @@ name = "autotype"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"mockall",
|
||||
"serial_test",
|
||||
"tracing",
|
||||
@@ -1026,6 +1027,12 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
@@ -1617,6 +1624,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
|
||||
@@ -39,6 +39,7 @@ futures = "=0.3.31"
|
||||
hex = "=0.4.3"
|
||||
homedir = "=0.3.6"
|
||||
interprocess = "=2.2.1"
|
||||
itertools = "=0.14.0"
|
||||
libc = "=0.2.178"
|
||||
linux-keyutils = "=0.2.4"
|
||||
memsec = "=0.7.0"
|
||||
|
||||
@@ -9,6 +9,7 @@ publish.workspace = true
|
||||
anyhow = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
itertools.workspace = true
|
||||
mockall = "=0.14.0"
|
||||
serial_test = "=3.2.0"
|
||||
tracing.workspace = true
|
||||
|
||||
@@ -28,6 +28,6 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
/// This function returns an `anyhow::Error` if there is any
|
||||
/// issue in typing the input. Detailed reasons will
|
||||
/// vary based on platform implementation.
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
windowing::type_input(input, keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support Linux autotype");
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ pub fn get_foreground_window_title() -> anyhow::Result<String> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
pub fn type_input(_input: Vec<u16>, _keyboard_shortcut: Vec<String>) -> anyhow::Result<()> {
|
||||
pub fn type_input(_input: &[u16], _keyboard_shortcut: &[String]) -> anyhow::Result<()> {
|
||||
todo!("Bitwarden does not yet support macOS autotype");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use tracing::debug;
|
||||
use windows::Win32::Foundation::{GetLastError, SetLastError, WIN32_ERROR};
|
||||
use windows::Win32::{
|
||||
Foundation::{GetLastError, SetLastError, WIN32_ERROR},
|
||||
UI::Input::KeyboardAndMouse::INPUT,
|
||||
};
|
||||
|
||||
mod type_input;
|
||||
mod window_title;
|
||||
@@ -12,7 +16,7 @@ const WIN32_SUCCESS: WIN32_ERROR = WIN32_ERROR(0);
|
||||
/// win32 errors.
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait ErrorOperations {
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setlasterror>
|
||||
fn set_last_error(err: u32) {
|
||||
debug!(err, "Calling SetLastError");
|
||||
unsafe {
|
||||
@@ -20,7 +24,7 @@ trait ErrorOperations {
|
||||
}
|
||||
}
|
||||
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror>
|
||||
fn get_last_error() -> WIN32_ERROR {
|
||||
let last_err = unsafe { GetLastError() };
|
||||
debug!("GetLastError(): {}", last_err.to_hresult().message());
|
||||
@@ -36,6 +40,23 @@ pub fn get_foreground_window_title() -> Result<String> {
|
||||
window_title::get_foreground_window_title()
|
||||
}
|
||||
|
||||
pub fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
type_input::type_input(input, keyboard_shortcut)
|
||||
/// `KeyboardShortcutInput` is an `INPUT` of one of the valid shortcut keys:
|
||||
/// - Control
|
||||
/// - Alt
|
||||
/// - Super
|
||||
/// - Shift
|
||||
/// - \[a-z\]\[A-Z\]
|
||||
struct KeyboardShortcutInput(INPUT);
|
||||
|
||||
pub fn type_input(input: &[u16], keyboard_shortcut: &[String]) -> Result<()> {
|
||||
debug!(?keyboard_shortcut, "type_input() called.");
|
||||
|
||||
// convert the raw string input to Windows input and error
|
||||
// if any key is not a valid keyboard shortcut input
|
||||
let keyboard_shortcut: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(s.as_str()))
|
||||
.try_collect()?;
|
||||
|
||||
type_input::type_input(input, &keyboard_shortcut)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,15 @@ use windows::Win32::UI::Input::KeyboardAndMouse::{
|
||||
VIRTUAL_KEY,
|
||||
};
|
||||
|
||||
use super::{ErrorOperations, Win32ErrorOperations};
|
||||
use super::{ErrorOperations, KeyboardShortcutInput, Win32ErrorOperations};
|
||||
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
|
||||
const IS_VIRTUAL_KEY: bool = true;
|
||||
const IS_REAL_KEY: bool = false;
|
||||
|
||||
/// `InputOperations` provides an interface to Window32 API for
|
||||
/// working with inputs.
|
||||
@@ -13,7 +21,7 @@ use super::{ErrorOperations, Win32ErrorOperations};
|
||||
trait InputOperations {
|
||||
/// Attempts to type the provided input wherever the user's cursor is.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
fn send_input(inputs: &[INPUT]) -> u32;
|
||||
}
|
||||
|
||||
@@ -21,8 +29,11 @@ struct Win32InputOperations;
|
||||
|
||||
impl InputOperations for Win32InputOperations {
|
||||
fn send_input(inputs: &[INPUT]) -> u32 {
|
||||
const INPUT_STRUCT_SIZE: i32 = std::mem::size_of::<INPUT>() as i32;
|
||||
let insert_count = unsafe { SendInput(inputs, INPUT_STRUCT_SIZE) };
|
||||
const INPUT_STRUCT_SIZE: usize = std::mem::size_of::<INPUT>();
|
||||
|
||||
let size = i32::try_from(INPUT_STRUCT_SIZE).expect("INPUT size to fit in i32");
|
||||
|
||||
let insert_count = unsafe { SendInput(inputs, size) };
|
||||
|
||||
debug!(insert_count, "SendInput() called.");
|
||||
|
||||
@@ -33,40 +44,37 @@ impl InputOperations for Win32InputOperations {
|
||||
/// Attempts to type the input text wherever the user's cursor is.
|
||||
///
|
||||
/// `input` must be a vector of utf-16 encoded characters to insert.
|
||||
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super,
|
||||
/// Shift, letters a - Z
|
||||
/// `keyboard_shortcut` is a vector of valid shortcut keys.
|
||||
///
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
pub(super) fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
pub(super) fn type_input(input: &[u16], keyboard_shortcut: &[KeyboardShortcutInput]) -> Result<()> {
|
||||
// the length of this vec is always shortcut keys to release + (2x length of input chars)
|
||||
let mut keyboard_inputs: Vec<INPUT> =
|
||||
Vec::with_capacity(keyboard_shortcut.len() + (input.len() * 2));
|
||||
|
||||
debug!(?keyboard_shortcut, "Converting keyboard shortcut to input.");
|
||||
|
||||
// Add key "up" inputs for the shortcut
|
||||
for key in keyboard_shortcut {
|
||||
keyboard_inputs.push(convert_shortcut_key_to_up_input(key)?);
|
||||
// insert the keyboard shortcut
|
||||
for shortcut in keyboard_shortcut {
|
||||
keyboard_inputs.push(shortcut.0);
|
||||
}
|
||||
|
||||
add_input(&input, &mut keyboard_inputs);
|
||||
add_input(input, &mut keyboard_inputs);
|
||||
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(keyboard_inputs)
|
||||
send_input::<Win32InputOperations, Win32ErrorOperations>(&keyboard_inputs)
|
||||
}
|
||||
|
||||
// Add key "down" and "up" inputs for the input
|
||||
// (currently in this form: {username}/t{password})
|
||||
fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
const TAB_KEY: u8 = 9;
|
||||
const TAB_KEY: u16 = 9;
|
||||
|
||||
for i in input {
|
||||
let next_down_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i as u8)
|
||||
let next_down_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Down, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Down, *i)
|
||||
};
|
||||
let next_up_input = if *i == TAB_KEY.into() {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i as u8)
|
||||
let next_up_input = if *i == TAB_KEY {
|
||||
build_virtual_key_input(InputKeyPress::Up, *i)
|
||||
} else {
|
||||
build_unicode_input(InputKeyPress::Up, *i)
|
||||
};
|
||||
@@ -76,26 +84,27 @@ fn add_input(input: &[u16], keyboard_inputs: &mut Vec<INPUT>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a valid shortcut key to an "up" keyboard input.
|
||||
///
|
||||
/// `input` must be a valid shortcut key: Control, Alt, Super, Shift, letters [a-z][A-Z]
|
||||
fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
const SHIFT_KEY: u8 = 0x10;
|
||||
const SHIFT_KEY_STR: &str = "Shift";
|
||||
const CONTROL_KEY: u8 = 0x11;
|
||||
const CONTROL_KEY_STR: &str = "Control";
|
||||
const ALT_KEY: u8 = 0x12;
|
||||
const ALT_KEY_STR: &str = "Alt";
|
||||
const LEFT_WINDOWS_KEY: u8 = 0x5B;
|
||||
const LEFT_WINDOWS_KEY_STR: &str = "Super";
|
||||
impl TryFrom<&str> for KeyboardShortcutInput {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
Ok(match key.as_str() {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
})
|
||||
fn try_from(key: &str) -> std::result::Result<Self, Self::Error> {
|
||||
const SHIFT_KEY: u16 = 0x10;
|
||||
const CONTROL_KEY: u16 = 0x11;
|
||||
const ALT_KEY: u16 = 0x12;
|
||||
const LEFT_WINDOWS_KEY: u16 = 0x5B;
|
||||
|
||||
// the modifier keys are using the Up keypress variant because the user has already
|
||||
// pressed those keys in order to trigger the feature.
|
||||
let input = match key {
|
||||
SHIFT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, SHIFT_KEY),
|
||||
CONTROL_KEY_STR => build_virtual_key_input(InputKeyPress::Up, CONTROL_KEY),
|
||||
ALT_KEY_STR => build_virtual_key_input(InputKeyPress::Up, ALT_KEY),
|
||||
LEFT_WINDOWS_KEY_STR => build_virtual_key_input(InputKeyPress::Up, LEFT_WINDOWS_KEY),
|
||||
_ => build_unicode_input(InputKeyPress::Up, get_alphabetic_hotkey(key)?),
|
||||
};
|
||||
|
||||
Ok(KeyboardShortcutInput(input))
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a letter that is a String, get the utf16 encoded
|
||||
@@ -105,7 +114,7 @@ fn convert_shortcut_key_to_up_input(key: String) -> Result<INPUT> {
|
||||
/// Because we only accept [a-z][A-Z], the decimal u16
|
||||
/// cast of the letter is safe because the unicode code point
|
||||
/// of these characters fits in a u16.
|
||||
fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
fn get_alphabetic_hotkey(letter: &str) -> Result<u16> {
|
||||
if letter.len() != 1 {
|
||||
error!(
|
||||
len = letter.len(),
|
||||
@@ -135,23 +144,28 @@ fn get_alphabetic_hotkey(letter: String) -> Result<u16> {
|
||||
}
|
||||
|
||||
/// An input key can be either pressed (down), or released (up).
|
||||
#[derive(Copy, Clone)]
|
||||
enum InputKeyPress {
|
||||
Down,
|
||||
Up,
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard unicode INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
/// Before modifying this function, make sure you read the `SendInput()` documentation:
|
||||
/// <https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput>
|
||||
/// <https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes>
|
||||
fn build_input(key_press: InputKeyPress, character: u16, is_virtual: bool) -> INPUT {
|
||||
let (w_vk, w_scan) = if is_virtual {
|
||||
(VIRTUAL_KEY(character), 0)
|
||||
} else {
|
||||
(VIRTUAL_KEY::default(), character)
|
||||
};
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -162,8 +176,8 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: Default::default(),
|
||||
wScan: character,
|
||||
wVk: w_vk,
|
||||
wScan: w_scan,
|
||||
dwFlags: KEYEVENTF_KEYUP | KEYEVENTF_UNICODE,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
@@ -173,53 +187,29 @@ fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
}
|
||||
}
|
||||
|
||||
/// A function for easily building keyboard virtual-key INPUT structs used in SendInput().
|
||||
///
|
||||
/// Before modifying this function, make sure you read the SendInput() documentation:
|
||||
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
|
||||
/// https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, virtual_key: u8) -> INPUT {
|
||||
match key_press {
|
||||
InputKeyPress::Down => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: Default::default(),
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
InputKeyPress::Up => INPUT {
|
||||
r#type: INPUT_KEYBOARD,
|
||||
Anonymous: INPUT_0 {
|
||||
ki: KEYBDINPUT {
|
||||
wVk: VIRTUAL_KEY(virtual_key as u16),
|
||||
wScan: Default::default(),
|
||||
dwFlags: KEYEVENTF_KEYUP,
|
||||
time: 0,
|
||||
dwExtraInfo: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
/// A function for easily building keyboard unicode `INPUT` structs used in `SendInput()`.
|
||||
fn build_unicode_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_REAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: Vec<INPUT>) -> Result<()>
|
||||
/// A function for easily building keyboard virtual-key `INPUT` structs used in `SendInput()`.
|
||||
fn build_virtual_key_input(key_press: InputKeyPress, character: u16) -> INPUT {
|
||||
build_input(key_press, character, IS_VIRTUAL_KEY)
|
||||
}
|
||||
|
||||
fn send_input<I, E>(inputs: &[INPUT]) -> Result<()>
|
||||
where
|
||||
I: InputOperations,
|
||||
E: ErrorOperations,
|
||||
{
|
||||
let insert_count = I::send_input(&inputs);
|
||||
let insert_count = I::send_input(inputs);
|
||||
|
||||
if insert_count == 0 {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(GetLastError = %last_err, "SendInput sent 0 inputs. Input was blocked by another thread.");
|
||||
|
||||
return Err(anyhow!("SendInput sent 0 inputs. Input was blocked by another thread. GetLastError: {last_err}"));
|
||||
} else if insert_count != inputs.len() as u32 {
|
||||
} else if insert_count != u32::try_from(inputs.len()).expect("to convert inputs len to u32") {
|
||||
let last_err = E::get_last_error().to_hresult().message();
|
||||
error!(sent = %insert_count, expected = inputs.len(), GetLastError = %last_err,
|
||||
"SendInput sent does not match expected."
|
||||
@@ -237,8 +227,9 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use itertools::Itertools;
|
||||
use serial_test::serial;
|
||||
use windows::Win32::Foundation::WIN32_ERROR;
|
||||
|
||||
@@ -249,7 +240,7 @@ mod tests {
|
||||
fn get_alphabetic_hot_key_succeeds() {
|
||||
for c in ('a'..='z').chain('A'..='Z') {
|
||||
let letter = c.to_string();
|
||||
let converted = get_alphabetic_hotkey(letter).unwrap();
|
||||
let converted = get_alphabetic_hotkey(&letter).unwrap();
|
||||
assert_eq!(converted, c as u16);
|
||||
}
|
||||
}
|
||||
@@ -258,14 +249,14 @@ mod tests {
|
||||
#[should_panic = "Final keyboard shortcut key should be a single character: foo"]
|
||||
fn get_alphabetic_hot_key_fail_not_single_char() {
|
||||
let letter = String::from("foo");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '}'"]
|
||||
fn get_alphabetic_hot_key_fail_not_alphabetic() {
|
||||
let letter = String::from("}");
|
||||
get_alphabetic_hotkey(letter).unwrap();
|
||||
get_alphabetic_hotkey(&letter).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +266,7 @@ mod tests {
|
||||
ctxi.checkpoint();
|
||||
ctxi.expect().returning(|_| 1);
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -284,6 +275,29 @@ mod tests {
|
||||
drop(ctxi);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn keyboard_shortcut_conversion_succeeds() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "B"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic = "Letter is not ASCII Alphabetic ([a-z][A-Z]): '1'"]
|
||||
fn keyboard_shortcut_conversion_fails_invalid_key() {
|
||||
let keyboard_shortcut = [CONTROL_KEY_STR, SHIFT_KEY_STR, "1"];
|
||||
let _: Vec<KeyboardShortcutInput> = keyboard_shortcut
|
||||
.iter()
|
||||
.map(|s| KeyboardShortcutInput::try_from(*s))
|
||||
.try_collect()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[should_panic(
|
||||
@@ -298,7 +312,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
@@ -320,7 +334,7 @@ mod tests {
|
||||
ctxge.checkpoint();
|
||||
ctxge.expect().returning(|| WIN32_ERROR(1));
|
||||
|
||||
send_input::<MockInputOperations, MockErrorOperations>(vec![build_unicode_input(
|
||||
send_input::<MockInputOperations, MockErrorOperations>(&[build_unicode_input(
|
||||
InputKeyPress::Up,
|
||||
0,
|
||||
)])
|
||||
|
||||
@@ -11,10 +11,10 @@ use super::{ErrorOperations, Win32ErrorOperations, WIN32_SUCCESS};
|
||||
|
||||
#[cfg_attr(test, mockall::automock)]
|
||||
trait WindowHandleOperations {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextlengthw>
|
||||
fn get_window_text_length_w(&self) -> Result<i32>;
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowtextw>
|
||||
fn get_window_text_w(&self, buffer: &mut Vec<u16>) -> Result<i32>;
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ pub(super) fn get_foreground_window_title() -> Result<String> {
|
||||
|
||||
/// Retrieves the foreground window handle and validates it.
|
||||
fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
|
||||
// <https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow>
|
||||
let handle = unsafe { GetForegroundWindow() };
|
||||
|
||||
debug!("GetForegroundWindow() called.");
|
||||
@@ -87,7 +87,7 @@ fn get_foreground_window_handle() -> Result<WindowHandle> {
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the length zero and GetLastError() != 0, return the GetLastError() message.
|
||||
/// - If the length zero and `GetLastError()` != 0, return the `GetLastError()` message.
|
||||
fn get_window_title_length<H, E>(window_handle: &H) -> Result<usize>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -128,7 +128,7 @@ where
|
||||
/// # Errors
|
||||
///
|
||||
/// - If the actual window title length (what the win32 API declares was written into the buffer),
|
||||
/// is length zero and GetLastError() != 0 , return the GetLastError() message.
|
||||
/// is length zero and `GetLastError()` != 0 , return the `GetLastError()` message.
|
||||
fn get_window_title<H, E>(window_handle: &H, expected_title_length: usize) -> Result<String>
|
||||
where
|
||||
H: WindowHandleOperations,
|
||||
@@ -140,7 +140,7 @@ where
|
||||
// The upstream will make a contains comparison on what we return, so an empty string
|
||||
// will not result on a match.
|
||||
warn!("Window title length is zero.");
|
||||
return Ok(String::from(""));
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u16> = vec![0; expected_title_length + 1]; // add extra space for the null character
|
||||
@@ -171,7 +171,7 @@ where
|
||||
mod tests {
|
||||
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
|
||||
//! crate in order to mock those, since the mock expectations set have to be global in
|
||||
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
|
||||
//! absence of a `self`. More info: <https://docs.rs/mockall/latest/mockall/#static-methods>
|
||||
|
||||
use mockall::predicate;
|
||||
use serial_test::serial;
|
||||
|
||||
@@ -1241,8 +1241,7 @@ pub mod autotype {
|
||||
input: Vec<u16>,
|
||||
keyboard_shortcut: Vec<String>,
|
||||
) -> napi::Result<(), napi::Status> {
|
||||
autotype::type_input(input, keyboard_shortcut).map_err(|_| {
|
||||
napi::Error::from_reason("Autotype Error: failed to type input".to_string())
|
||||
})
|
||||
autotype::type_input(&input, &keyboard_shortcut)
|
||||
.map_err(|e| napi::Error::from_reason(format!("Autotype Error: {e}")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden Beta",
|
||||
"appId": "com.bitwarden.desktop.beta",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"productName": "Bitwarden",
|
||||
"appId": "com.bitwarden.desktop",
|
||||
"buildDependenciesFromSource": true,
|
||||
"copyright": "Copyright © 2015-2025 Bitwarden Inc.",
|
||||
"copyright": "Copyright © 2015-2026 Bitwarden Inc.",
|
||||
"directories": {
|
||||
"buildResources": "resources",
|
||||
"output": "dist",
|
||||
|
||||
@@ -4,7 +4,9 @@ import { RouterModule } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendFiltersNavComponent } from "../tools/send-v2/send-filters-nav.component";
|
||||
|
||||
@@ -36,6 +38,8 @@ describe("DesktopLayoutComponent", () => {
|
||||
let component: DesktopLayoutComponent;
|
||||
let fixture: ComponentFixture<DesktopLayoutComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopLayoutComponent, RouterModule.forRoot([]), NavigationModule],
|
||||
@@ -44,6 +48,10 @@ describe("DesktopLayoutComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(DesktopLayoutComponent, {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { DesktopSideNavComponent } from "./desktop-side-nav.component";
|
||||
|
||||
@@ -24,6 +26,8 @@ describe("DesktopSideNavComponent", () => {
|
||||
let component: DesktopSideNavComponent;
|
||||
let fixture: ComponentFixture<DesktopSideNavComponent>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DesktopSideNavComponent, NavigationModule],
|
||||
@@ -32,6 +36,10 @@ describe("DesktopSideNavComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import { RouterTestingHarness } from "@angular/router/testing";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { SendType } from "@bitwarden/common/tools/send/enums/send-type";
|
||||
import { NavigationModule } from "@bitwarden/components";
|
||||
import { SendListFiltersService } from "@bitwarden/send-ui";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { SendFiltersNavComponent } from "./send-filters-nav.component";
|
||||
|
||||
@@ -35,6 +37,8 @@ describe("SendFiltersNavComponent", () => {
|
||||
let filterFormValueSubject: BehaviorSubject<{ sendType: SendType | null }>;
|
||||
let mockSendListFiltersService: Partial<SendListFiltersService>;
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
beforeEach(async () => {
|
||||
filterFormValueSubject = new BehaviorSubject<{ sendType: SendType | null }>({
|
||||
sendType: null,
|
||||
@@ -72,6 +76,10 @@ describe("SendFiltersNavComponent", () => {
|
||||
t: jest.fn((key) => key),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
|
||||
@@ -4388,6 +4388,9 @@
|
||||
"sessionTimeoutHeader": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"sessionTimeoutSettingsManagedByOrganization": {
|
||||
"message": "This setting is managed by your organization."
|
||||
},
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
"message": "添加附件"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"fixEncryption": {
|
||||
"message": "修复加密"
|
||||
@@ -4454,7 +4454,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4463,7 +4463,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -4472,7 +4472,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
|
||||
@@ -557,7 +557,7 @@ export class VaultV2Component<C extends CipherViewLike>
|
||||
}
|
||||
}
|
||||
|
||||
if (!cipher.organizationId && !cipher.isDeleted && !cipher.isArchived) {
|
||||
if (userCanArchive && !cipher.isDeleted && !cipher.isArchived) {
|
||||
menu.push({
|
||||
label: this.i18nService.t("archiveVerb"),
|
||||
click: async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<authors>Bitwarden Inc.</authors>
|
||||
<projectUrl>https://bitwarden.com/</projectUrl>
|
||||
<iconUrl>https://raw.githubusercontent.com/bitwarden/brand/master/icons/256x256.png</iconUrl>
|
||||
<copyright>Copyright © 2015-2025 Bitwarden Inc.</copyright>
|
||||
<copyright>Copyright © 2015-2026 Bitwarden Inc.</copyright>
|
||||
<projectSourceUrl>https://github.com/bitwarden/clients/</projectSourceUrl>
|
||||
<docsUrl>https://bitwarden.com/help/</docsUrl>
|
||||
<bugTrackerUrl>https://github.com/bitwarden/clients/issues</bugTrackerUrl>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.12.1",
|
||||
"version": "2025.12.2",
|
||||
"scripts": {
|
||||
"build:oss": "webpack",
|
||||
"build:bit": "webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -587,6 +587,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -16,27 +16,26 @@
|
||||
<img class="tw-float-right tw-ml-5 mfaType7" alt="FIDO2 WebAuthn logo" />
|
||||
<ul class="bwi-ul">
|
||||
<li *ngFor="let k of keys; let i = index" #removeKeyBtn [appApiAction]="k.removePromise">
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<span *ngIf="!k.configured || !k.name" bitTypography="body1" class="tw-font-medium">
|
||||
{{ "webAuthnkeyX" | i18n: (i + 1).toString() }}
|
||||
</span>
|
||||
<span *ngIf="k.configured && k.name" bitTypography="body1" class="tw-font-medium">
|
||||
{{ k.name }}
|
||||
</span>
|
||||
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
<ng-container *ngIf="k.configured">
|
||||
<i class="bwi bwi-li bwi-key"></i>
|
||||
<span *ngIf="k.configured" bitTypography="body1" class="tw-font-medium">
|
||||
{{ k.name || ("unnamedKey" | i18n) }}
|
||||
</span>
|
||||
<ng-container *ngIf="k.configured && !$any(removeKeyBtn).loading">
|
||||
<ng-container *ngIf="k.migrated">
|
||||
<span>{{ "webAuthnMigrated" | i18n }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(removeKeyBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
-
|
||||
<a bitLink href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="keysConfiguredCount > 1 && k.configured">
|
||||
<i
|
||||
class="bwi bwi-spin bwi-spinner tw-text-muted bwi-fw"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
*ngIf="$any(removeKeyBtn).loading"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
-
|
||||
<a bitLink href="#" appStopClick (click)="remove(k)">{{ "remove" | i18n }}</a>
|
||||
</ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -60,7 +59,9 @@
|
||||
type="button"
|
||||
[bitAction]="readKey"
|
||||
buttonType="secondary"
|
||||
[disabled]="$any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable"
|
||||
[disabled]="
|
||||
$any(readKeyBtn).loading() || webAuthnListening || !keyIdAvailable || formGroup.invalid
|
||||
"
|
||||
class="tw-mr-2"
|
||||
#readKeyBtn
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, NgZone } from "@angular/core";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
@@ -99,7 +99,7 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
toastService,
|
||||
);
|
||||
this.formGroup = new FormGroup({
|
||||
name: new FormControl({ value: "", disabled: false }),
|
||||
name: new FormControl({ value: "", disabled: false }, Validators.required),
|
||||
});
|
||||
this.auth(data);
|
||||
}
|
||||
@@ -213,7 +213,22 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
this.webAuthnListening = listening;
|
||||
}
|
||||
|
||||
private findNextAvailableKeyId(existingIds: Set<number>): number {
|
||||
// Search for first gap, bounded by current key count + 1
|
||||
for (let i = 1; i <= existingIds.size + 1; i++) {
|
||||
if (!existingIds.has(i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached due to loop bounds, but TypeScript requires a return
|
||||
throw new Error("Unable to find next available key ID");
|
||||
}
|
||||
|
||||
private processResponse(response: TwoFactorWebAuthnResponse) {
|
||||
if (!response.keys || response.keys.length === 0) {
|
||||
response.keys = [];
|
||||
}
|
||||
this.resetWebAuthn();
|
||||
this.keys = [];
|
||||
this.keyIdAvailable = null;
|
||||
@@ -223,26 +238,37 @@ export class TwoFactorSetupWebAuthnComponent extends TwoFactorSetupMethodBaseCom
|
||||
nameControl.setValue("");
|
||||
}
|
||||
this.keysConfiguredCount = 0;
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
if (response.keys != null) {
|
||||
const key = response.keys.filter((k) => k.id === i);
|
||||
if (key.length > 0) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: i,
|
||||
name: key[0].name,
|
||||
configured: true,
|
||||
migrated: key[0].migrated,
|
||||
removePromise: null,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
this.keys.push({ id: i, name: "", configured: false, removePromise: null });
|
||||
if (this.keyIdAvailable == null) {
|
||||
this.keyIdAvailable = i;
|
||||
}
|
||||
|
||||
// Build configured keys
|
||||
for (const key of response.keys) {
|
||||
this.keysConfiguredCount++;
|
||||
this.keys.push({
|
||||
id: key.id,
|
||||
name: key.name,
|
||||
configured: true,
|
||||
migrated: key.migrated,
|
||||
removePromise: null,
|
||||
});
|
||||
}
|
||||
|
||||
// [PM-20109]: To accommodate the existing form logic with minimal changes,
|
||||
// we need to have at least one unconfigured key slot available to the collection.
|
||||
// Prior to PM-20109, both client and server had hard checks for IDs <= 5.
|
||||
// While we don't have any technical constraints _at this time_, we should avoid
|
||||
// unbounded growth of key IDs over time as users add/remove keys;
|
||||
// this strategy gap-fills key IDs.
|
||||
const existingIds = new Set(response.keys.map((k) => k.id));
|
||||
const nextId = this.findNextAvailableKeyId(existingIds);
|
||||
|
||||
// Add unconfigured slot, which can be used to add a new key
|
||||
this.keys.push({
|
||||
id: nextId,
|
||||
name: "",
|
||||
configured: false,
|
||||
removePromise: null,
|
||||
});
|
||||
this.keyIdAvailable = nextId;
|
||||
|
||||
this.enabled = response.enabled;
|
||||
this.onUpdated.emit(this.enabled);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
|
||||
<ng-container header>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</bit-toggle>
|
||||
</ng-container>
|
||||
</bit-toggle-group>
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="53">
|
||||
<bit-table-scroll [dataSource]="dataSource" [rowSize]="75">
|
||||
<ng-container header>
|
||||
<th bitCell></th>
|
||||
<th bitCell bitSortable="name">{{ "name" | i18n }}</th>
|
||||
|
||||
@@ -132,7 +132,10 @@ describe("CipherStep", () => {
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
@@ -144,14 +147,39 @@ describe("CipherStep", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true when there are undecryptable ciphers", async () => {
|
||||
it("returns true when there are undecryptable ciphers but at least one decryptable cipher", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [{ id: "cipher-1", organizationId: null } as Cipher],
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
cipherEncryptionService.decrypt.mockRejectedValueOnce(new Error("Decryption failed"));
|
||||
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when all ciphers are undecryptable", async () => {
|
||||
const userId = "user-id" as UserId;
|
||||
const workingData: RecoveryWorkingData = {
|
||||
userId,
|
||||
userKey: null,
|
||||
encryptedPrivateKey: null,
|
||||
isPrivateKeyCorrupt: false,
|
||||
ciphers: [
|
||||
{ id: "cipher-1", organizationId: null } as Cipher,
|
||||
{ id: "cipher-2", organizationId: null } as Cipher,
|
||||
],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
@@ -160,7 +188,7 @@ describe("CipherStep", () => {
|
||||
await cipherStep.runDiagnostics(workingData, logger);
|
||||
const result = cipherStep.canRecover(workingData);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export class CipherStep implements RecoveryStep {
|
||||
title = "recoveryStepCipherTitle";
|
||||
|
||||
private undecryptableCipherIds: string[] = [];
|
||||
private decryptableCipherIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -31,18 +32,21 @@ export class CipherStep implements RecoveryStep {
|
||||
for (const cipher of userCiphers) {
|
||||
try {
|
||||
await this.cipherService.decrypt(cipher, workingData.userId);
|
||||
this.decryptableCipherIds.push(cipher.id);
|
||||
} catch {
|
||||
logger.record(`Cipher ID ${cipher.id} was undecryptable`);
|
||||
this.undecryptableCipherIds.push(cipher.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableCipherIds.length} undecryptable ciphers`);
|
||||
logger.record(`Found ${this.decryptableCipherIds.length} decryptable ciphers`);
|
||||
|
||||
return this.undecryptableCipherIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableCipherIds.length > 0;
|
||||
// If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
|
||||
return this.undecryptableCipherIds.length > 0 && this.decryptableCipherIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
|
||||
@@ -11,6 +11,7 @@ export class FolderStep implements RecoveryStep {
|
||||
title = "recoveryStepFoldersTitle";
|
||||
|
||||
private undecryptableFolderIds: string[] = [];
|
||||
private decryptableFolderIds: string[] = [];
|
||||
|
||||
constructor(
|
||||
private folderService: FolderApiServiceAbstraction,
|
||||
@@ -36,18 +37,21 @@ export class FolderStep implements RecoveryStep {
|
||||
folder.name.encryptedString,
|
||||
workingData.userKey.toEncoded(),
|
||||
);
|
||||
this.decryptableFolderIds.push(folder.id);
|
||||
} catch {
|
||||
logger.record(`Folder name for folder ID ${folder.id} was undecryptable`);
|
||||
this.undecryptableFolderIds.push(folder.id);
|
||||
}
|
||||
}
|
||||
logger.record(`Found ${this.undecryptableFolderIds.length} undecryptable folders`);
|
||||
logger.record(`Found ${this.decryptableFolderIds.length} decryptable folders`);
|
||||
|
||||
return this.undecryptableFolderIds.length == 0;
|
||||
}
|
||||
|
||||
canRecover(workingData: RecoveryWorkingData): boolean {
|
||||
return this.undecryptableFolderIds.length > 0;
|
||||
// If everything fails to decrypt, it's a deeper issue and we shouldn't offer recovery here.
|
||||
return this.undecryptableFolderIds.length > 0 && this.decryptableFolderIds.length > 0;
|
||||
}
|
||||
|
||||
async runRecovery(workingData: RecoveryWorkingData, logger: LogRecorder): Promise<void> {
|
||||
|
||||
@@ -7,10 +7,12 @@ import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||
import { IconButtonModule, NavigationModule } from "@bitwarden/components";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { NavItemComponent } from "@bitwarden/components/src/navigation/nav-item.component";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
|
||||
import { ProductSwitcherItem, ProductSwitcherService } from "../shared/product-switcher.service";
|
||||
|
||||
@@ -59,6 +61,8 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
productSwitcherService.shouldShowPremiumUpgradeButton$ = mockShouldShowPremiumUpgradeButton$;
|
||||
mockProducts$.next({ bento: [], other: [] });
|
||||
|
||||
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [RouterModule, NavigationModule, IconButtonModule, MockUpgradeNavButtonComponent],
|
||||
declarations: [NavigationProductSwitcherComponent, I18nPipe],
|
||||
@@ -72,6 +76,10 @@ describe("NavigationProductSwitcherComponent", () => {
|
||||
provide: ActivatedRoute,
|
||||
useValue: mock<ActivatedRoute>(),
|
||||
},
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useValue: fakeGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
@@ -16,10 +16,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
import {
|
||||
LayoutComponent,
|
||||
NavigationModule,
|
||||
StorybookGlobalStateProvider,
|
||||
} from "@bitwarden/components";
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { I18nMockService } from "@bitwarden/components/src/utils/i18n-mock.service";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { ProductSwitcherService } from "../shared/product-switcher.service";
|
||||
@@ -82,7 +87,7 @@ class MockAccountService implements Partial<AccountService> {
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,6 +188,10 @@ export default {
|
||||
},
|
||||
]),
|
||||
),
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useClass: StorybookGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -82,7 +82,7 @@ class MockAccountService implements Partial<AccountService> {
|
||||
name: "Test User 1",
|
||||
email: "test@email.com",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@ import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
|
||||
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { CipherViewLike } from "@bitwarden/common/vault/utils/cipher-view-like-utils";
|
||||
import { LayoutComponent } from "@bitwarden/components";
|
||||
import { LayoutComponent, StorybookGlobalStateProvider } from "@bitwarden/components";
|
||||
import { GlobalStateProvider } from "@bitwarden/state";
|
||||
import { RoutedVaultFilterService } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/services/routed-vault-filter.service";
|
||||
|
||||
import { GroupView } from "../../../admin-console/organizations/core";
|
||||
@@ -168,6 +169,10 @@ export default {
|
||||
providers: [
|
||||
importProvidersFrom(RouterModule.forRoot([], { useHash: true })),
|
||||
importProvidersFrom(PreloadedEnglishI18nModule),
|
||||
{
|
||||
provide: GlobalStateProvider,
|
||||
useClass: StorybookGlobalStateProvider,
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -74,6 +74,9 @@ export class RoutedVaultFilterService implements OnDestroy {
|
||||
type: filter.type ?? null,
|
||||
},
|
||||
queryParamsHandling: "merge",
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
};
|
||||
return [commands, extras];
|
||||
}
|
||||
|
||||
@@ -424,6 +424,9 @@ export class VaultComponent<C extends CipherViewLike> implements OnInit, OnDestr
|
||||
queryParams: { search: Utils.isNullOrEmpty(searchText) ? null : searchText },
|
||||
queryParamsHandling: "merge",
|
||||
replaceUrl: true,
|
||||
state: {
|
||||
focusMainAfterNav: false,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
`;
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -5871,7 +5871,7 @@
|
||||
"description": "This is the policy description shown in the policy list."
|
||||
},
|
||||
"organizationDataOwnershipDescContent": {
|
||||
"message": "All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection will be available for each member to store items. Learn more about managing the ",
|
||||
"message": "Bütün elementlər bir təşkilata məxsus olacaq və orada saxlanılacaq, bu da təşkilat üzrə kontrollar, görünürlük və hesabatları mümkün edəcək. İşə salındığı zaman, hər üzv üçün elementləri saxlaya biləcəyi ilkin bir kolleksiya mövcud olacaq. Daha ətraflı ",
|
||||
"description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'All items will be owned and saved to the organization, enabling organization-wide controls, visibility, and reporting. When turned on, a default collection will be available for each member to store items. Learn more about managing the credential lifecycle.'"
|
||||
},
|
||||
"organizationDataOwnershipContentAnchor": {
|
||||
@@ -6752,7 +6752,7 @@
|
||||
"message": "Bütün üzvlər üçün maksimum bitmə vaxtını \"Heç vaxt\" olaraq icazə vermək istədiyinizə əminsiniz?"
|
||||
},
|
||||
"sessionTimeoutConfirmationNeverDescription": {
|
||||
"message": "This option will save your members' encryption keys on their devices. If you choose this option, ensure that their devices are adequately protected."
|
||||
"message": "Bu seçim, üzvlərinizin şifrələmə açarlarını onların cihazlarında saxlayacaq. Bu seçimi seçsəniz, onların cihazlarının lazımi səviyyədə qorunduğuna əmin olun."
|
||||
},
|
||||
"learnMoreAboutDeviceProtection": {
|
||||
"message": "Cihaz mühafizəsi barədə daha ətraflı"
|
||||
|
||||
@@ -12436,7 +12436,7 @@
|
||||
"message": "Du hast Bitwarden Premium"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
"message": "Dein Premium-Abonnement anzeigen und verwalten"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "Du musst deine Lizenzdatei aktualisieren"
|
||||
@@ -12472,7 +12472,7 @@
|
||||
"message": "Du hast bereits ein Abonnement?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "Öffne die Abonnementseite in deinem Bitwarden Cloud-Konto und lade deine Lizenzdatei herunter. Gehe dann zu dieser Seite zurück und lade sie unten hoch."
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "Alle Tarife anzeigen"
|
||||
|
||||
@@ -2634,6 +2634,9 @@
|
||||
"key": {
|
||||
"message": "Key"
|
||||
},
|
||||
"unnamedKey": {
|
||||
"message": "Unnamed key"
|
||||
},
|
||||
"twoStepAuthenticatorEnterCodeV2": {
|
||||
"message": "Verification code"
|
||||
},
|
||||
@@ -12305,6 +12308,9 @@
|
||||
"userVerificationFailed": {
|
||||
"message": "User verification failed."
|
||||
},
|
||||
"resizeSideNavigation": {
|
||||
"message": "Resize side navigation"
|
||||
},
|
||||
"recoveryDeleteCiphersTitle": {
|
||||
"message": "Delete unrecoverable vault items"
|
||||
},
|
||||
|
||||
@@ -1970,11 +1970,11 @@
|
||||
"message": "Le chiavi di cifratura dell'account sono uniche per ogni account utente Bitwarden, quindi non è possibile importare un'esportazione cifrata in un account diverso."
|
||||
},
|
||||
"exportNoun": {
|
||||
"message": "Export",
|
||||
"message": "Esporta",
|
||||
"description": "The noun form of the word Export"
|
||||
},
|
||||
"exportVerb": {
|
||||
"message": "Export",
|
||||
"message": "Esporta",
|
||||
"description": "The verb form of the word Export"
|
||||
},
|
||||
"exportFrom": {
|
||||
@@ -2303,11 +2303,11 @@
|
||||
"message": "Strumenti"
|
||||
},
|
||||
"importNoun": {
|
||||
"message": "Import",
|
||||
"message": "Importa",
|
||||
"description": "The noun form of the word Import"
|
||||
},
|
||||
"importVerb": {
|
||||
"message": "Import",
|
||||
"message": "Importa",
|
||||
"description": "The verb form of the word Import"
|
||||
},
|
||||
"importData": {
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "Avvia abbonamento cloud"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "Avvia abbonamento cloud"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Spazio di archiviazione"
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "Trasferimento di proprietà all'organizzazione accettato."
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "Revocato per il rifiuto di trasferimento di proprietà all'organizzazione."
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "Utente $ID$ invitato.",
|
||||
@@ -11607,7 +11607,7 @@
|
||||
"message": "Togli dall'archivio"
|
||||
},
|
||||
"unArchiveAndSave": {
|
||||
"message": "Unarchive and save"
|
||||
"message": "Togli dall'archivio e salva"
|
||||
},
|
||||
"itemsInArchive": {
|
||||
"message": "Elementi archiviati"
|
||||
@@ -12251,43 +12251,43 @@
|
||||
}
|
||||
},
|
||||
"removeMasterPasswordForOrgUserKeyConnector": {
|
||||
"message": "Your organization is no longer using master passwords to log into Bitwarden. To continue, verify the organization and domain."
|
||||
"message": "La tua organizzazione non utilizza più le password principali per accedere a Bitwarden. Per continuare, verifica l'organizzazione e il dominio."
|
||||
},
|
||||
"continueWithLogIn": {
|
||||
"message": "Continue with log in"
|
||||
"message": "Accedi e continua"
|
||||
},
|
||||
"doNotContinue": {
|
||||
"message": "Do not continue"
|
||||
"message": "Non continuare"
|
||||
},
|
||||
"domain": {
|
||||
"message": "Domain"
|
||||
"message": "Dominio"
|
||||
},
|
||||
"keyConnectorDomainTooltip": {
|
||||
"message": "This domain will store your account encryption keys, so make sure you trust it. If you're not sure, check with your admin."
|
||||
"message": "Questo dominio memorizzerà le chiavi di crittografia del tuo account, quindi assicurati di impostarlo come affidabile. Se non hai la certezza che lo sia, verifica con l'amministratore."
|
||||
},
|
||||
"verifyYourOrganization": {
|
||||
"message": "Verify your organization to log in"
|
||||
"message": "Verifica la tua organizzazione per accedere"
|
||||
},
|
||||
"organizationVerified": {
|
||||
"message": "Organization verified"
|
||||
"message": "Organizzazione verificata"
|
||||
},
|
||||
"domainVerified": {
|
||||
"message": "Domain verified"
|
||||
"message": "Dominio verificato"
|
||||
},
|
||||
"leaveOrganizationContent": {
|
||||
"message": "If you don't verify your organization, your access to the organization will be revoked."
|
||||
"message": "Se non verifichi l'organizzazione, il tuo accesso sarà revocato."
|
||||
},
|
||||
"leaveNow": {
|
||||
"message": "Leave now"
|
||||
"message": "Abbandona"
|
||||
},
|
||||
"verifyYourDomainToLogin": {
|
||||
"message": "Verify your domain to log in"
|
||||
"message": "Verifica il tuo dominio per accedere"
|
||||
},
|
||||
"verifyYourDomainDescription": {
|
||||
"message": "To continue with log in, verify this domain."
|
||||
"message": "Per continuare con l'accesso, verifica questo dominio."
|
||||
},
|
||||
"confirmKeyConnectorOrganizationUserDescription": {
|
||||
"message": "To continue with log in, verify the organization and domain."
|
||||
"message": "Per continuare con l'accesso, verifica l'organizzazione e il dominio."
|
||||
},
|
||||
"confirmNoSelectedCriticalApplicationsTitle": {
|
||||
"message": "Non ci sono applicazioni contrassegnate come critiche"
|
||||
@@ -12433,13 +12433,13 @@
|
||||
"message": "Perché vedo questo avviso?"
|
||||
},
|
||||
"youHaveBitwardenPremium": {
|
||||
"message": "You have Bitwarden Premium"
|
||||
"message": "Hai Bitwarden Premium"
|
||||
},
|
||||
"viewAndManagePremiumSubscription": {
|
||||
"message": "View and manage your Premium subscription"
|
||||
"message": "Visualizza e gestisci il tuo abbonamento Premium"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "You'll need to update your license file"
|
||||
"message": "Dovrai aggiornare il tuo file di licenza"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$.",
|
||||
@@ -12451,16 +12451,16 @@
|
||||
}
|
||||
},
|
||||
"uploadLicenseFile": {
|
||||
"message": "Upload license file"
|
||||
"message": "Carica il file di licenza"
|
||||
},
|
||||
"uploadYourLicenseFile": {
|
||||
"message": "Upload your license file"
|
||||
"message": "Carica il file di licenza"
|
||||
},
|
||||
"uploadYourPremiumLicenseFile": {
|
||||
"message": "Upload your Premium license file"
|
||||
"message": "Carica il tuo file di licenza Premium"
|
||||
},
|
||||
"uploadLicenseFileDesc": {
|
||||
"message": "Your license file name will be similar to: $FILE_NAME$",
|
||||
"message": "Il nome del file di licenza sarà simile a $FILE_NAME$",
|
||||
"placeholders": {
|
||||
"file_name": {
|
||||
"content": "$1",
|
||||
@@ -12469,15 +12469,15 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "Already have a subscription?"
|
||||
"message": "Hai già un abbonamento?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "Open the subscription page on your Bitwarden cloud account and download your license file. Then return to this screen and upload it below."
|
||||
"message": "Vai alla pagina degli abbonamenti del tuo account Bitwarden e scarica il file di licenza, poi torna a caricarlo qui."
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "View all plans"
|
||||
"message": "Visualizza tutti i piani"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "Complete online security"
|
||||
"message": "Sicurezza online completa"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1943,7 +1943,7 @@
|
||||
"message": "Copiar UUID"
|
||||
},
|
||||
"errorRefreshingAccessToken": {
|
||||
"message": "Erro de recarregamento do token de acesso"
|
||||
"message": "Erro de Recarregamento do Token de Acesso"
|
||||
},
|
||||
"errorRefreshingAccessTokenDesc": {
|
||||
"message": "Nenhum token de atualização ou chave de API foi encontrado. Tente se desconectar e se conectar novamente."
|
||||
@@ -3294,7 +3294,7 @@
|
||||
"message": "Iniciar Assinatura na Nuvem"
|
||||
},
|
||||
"launchCloudSubscriptionSentenceCase": {
|
||||
"message": "Launch cloud subscription"
|
||||
"message": "Executar assinatura na nuvem"
|
||||
},
|
||||
"storage": {
|
||||
"message": "Armazenamento"
|
||||
|
||||
@@ -3862,7 +3862,7 @@
|
||||
"description": "Browser extension/addon"
|
||||
},
|
||||
"desktop": {
|
||||
"message": "桌面版应用",
|
||||
"message": "桌面端",
|
||||
"description": "Desktop app"
|
||||
},
|
||||
"webVault": {
|
||||
@@ -4212,10 +4212,10 @@
|
||||
}
|
||||
},
|
||||
"userAcceptedTransfer": {
|
||||
"message": "Accepted transfer to organization ownership."
|
||||
"message": "接受了转移至组织所有权。"
|
||||
},
|
||||
"userDeclinedTransfer": {
|
||||
"message": "Revoked for declining transfer to organization ownership."
|
||||
"message": "因拒绝转移至组织所有权而被撤销。"
|
||||
},
|
||||
"invitedUserId": {
|
||||
"message": "邀请了用户 $ID$。",
|
||||
@@ -5195,7 +5195,7 @@
|
||||
"message": "需要先修复您的密码库中的旧文件附件,然后才能轮换您账户的加密密钥。"
|
||||
},
|
||||
"itemsTransferred": {
|
||||
"message": "项目已传输"
|
||||
"message": "项目已转移"
|
||||
},
|
||||
"yourAccountsFingerprint": {
|
||||
"message": "您的账户指纹短语",
|
||||
@@ -6825,7 +6825,7 @@
|
||||
"message": "密码库超时不在允许的范围内。"
|
||||
},
|
||||
"disableExport": {
|
||||
"message": "移除导出"
|
||||
"message": "禁用导出"
|
||||
},
|
||||
"disablePersonalVaultExportDescription": {
|
||||
"message": "不允许成员从个人密码库导出数据。"
|
||||
@@ -12406,7 +12406,7 @@
|
||||
"message": "我该如何管理我的密码库?"
|
||||
},
|
||||
"transferItemsToOrganizationTitle": {
|
||||
"message": "传输项目到 $ORGANIZATION$",
|
||||
"message": "转移项目到 $ORGANIZATION$",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -12415,7 +12415,7 @@
|
||||
}
|
||||
},
|
||||
"transferItemsToOrganizationContent": {
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以传输您的项目的所有权。",
|
||||
"message": "出于安全和合规考虑,$ORGANIZATION$ 要求所有项目归组织所有。点击「接受」以转移您的项目的所有权。",
|
||||
"placeholders": {
|
||||
"organization": {
|
||||
"content": "$1",
|
||||
@@ -12424,7 +12424,7 @@
|
||||
}
|
||||
},
|
||||
"acceptTransfer": {
|
||||
"message": "接受传输"
|
||||
"message": "接受转移"
|
||||
},
|
||||
"declineAndLeave": {
|
||||
"message": "拒绝并退出"
|
||||
@@ -12439,7 +12439,7 @@
|
||||
"message": "查看和管理您的高级版订阅"
|
||||
},
|
||||
"youNeedToUpdateLicenseFile": {
|
||||
"message": "您需要更新您的许可文件"
|
||||
"message": "您需要更新您的许可证文件"
|
||||
},
|
||||
"youNeedToUpdateLicenseFileDate": {
|
||||
"message": "$DATE$。",
|
||||
@@ -12469,13 +12469,13 @@
|
||||
}
|
||||
},
|
||||
"alreadyHaveSubscriptionQuestion": {
|
||||
"message": "已经有一个订阅?"
|
||||
"message": "已经有一个订阅了吗?"
|
||||
},
|
||||
"alreadyHaveSubscriptionSelfHostedMessage": {
|
||||
"message": "打开您的 Bitwarden 云账户上的订阅页面并下载您的许可证文件,然后返回此屏幕并上传。"
|
||||
"message": "打开您的 Bitwarden 云账户中的订阅页面并下载您的许可证文件。然后返回此界面并在下方上传该文件。"
|
||||
},
|
||||
"viewAllPlans": {
|
||||
"message": "查看所有套餐"
|
||||
"message": "查看所有方案"
|
||||
},
|
||||
"planDescPremium": {
|
||||
"message": "全面的在线安全防护"
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
// Mock asUuid to return the input value for test consistency
|
||||
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk.service", () => ({
|
||||
asUuid: (x: any) => x,
|
||||
}));
|
||||
|
||||
import { DestroyRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import {
|
||||
LoginEmailServiceAbstraction,
|
||||
LogoutService,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import { SignedSecurityState } from "@bitwarden/common/key-management/types";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { AnonLayoutWrapperDataService, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { LoginDecryptionOptionsComponent } from "./login-decryption-options.component";
|
||||
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||
|
||||
describe("LoginDecryptionOptionsComponent", () => {
|
||||
let component: LoginDecryptionOptionsComponent;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let anonLayoutWrapperDataService: MockProxy<AnonLayoutWrapperDataService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let destroyRef: MockProxy<DestroyRef>;
|
||||
let deviceTrustService: MockProxy<DeviceTrustServiceAbstraction>;
|
||||
let dialogService: MockProxy<DialogService>;
|
||||
let formBuilder: FormBuilder;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let loginDecryptionOptionsService: MockProxy<LoginDecryptionOptionsService>;
|
||||
let loginEmailService: MockProxy<LoginEmailServiceAbstraction>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let passwordResetEnrollmentService: MockProxy<PasswordResetEnrollmentServiceAbstraction>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let router: MockProxy<Router>;
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
let userDecryptionOptionsService: MockProxy<UserDecryptionOptionsServiceAbstraction>;
|
||||
let validationService: MockProxy<ValidationService>;
|
||||
let logoutService: MockProxy<LogoutService>;
|
||||
let registerSdkService: MockProxy<RegisterSdkService>;
|
||||
let securityStateService: MockProxy<SecurityStateService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
let accountCryptographicStateService: MockProxy<any>;
|
||||
|
||||
const mockUserId = "user-id-123" as UserId;
|
||||
const mockEmail = "test@example.com";
|
||||
const mockOrgId = "org-id-456";
|
||||
|
||||
beforeEach(() => {
|
||||
accountService = mock<AccountService>();
|
||||
anonLayoutWrapperDataService = mock<AnonLayoutWrapperDataService>();
|
||||
apiService = mock<ApiService>();
|
||||
destroyRef = mock<DestroyRef>();
|
||||
deviceTrustService = mock<DeviceTrustServiceAbstraction>();
|
||||
dialogService = mock<DialogService>();
|
||||
formBuilder = new FormBuilder();
|
||||
i18nService = mock<I18nService>();
|
||||
keyService = mock<KeyService>();
|
||||
loginDecryptionOptionsService = mock<LoginDecryptionOptionsService>();
|
||||
loginEmailService = mock<LoginEmailServiceAbstraction>();
|
||||
messagingService = mock<MessagingService>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
passwordResetEnrollmentService = mock<PasswordResetEnrollmentServiceAbstraction>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
router = mock<Router>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
toastService = mock<ToastService>();
|
||||
userDecryptionOptionsService = mock<UserDecryptionOptionsServiceAbstraction>();
|
||||
validationService = mock<ValidationService>();
|
||||
logoutService = mock<LogoutService>();
|
||||
registerSdkService = mock<RegisterSdkService>();
|
||||
securityStateService = mock<SecurityStateService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
configService = mock<ConfigService>();
|
||||
accountCryptographicStateService = mock();
|
||||
|
||||
// Setup default mocks
|
||||
accountService.activeAccount$ = new BehaviorSubject({
|
||||
id: mockUserId,
|
||||
email: mockEmail,
|
||||
name: "Test User",
|
||||
emailVerified: true,
|
||||
creationDate: new Date(),
|
||||
});
|
||||
platformUtilsService.getClientType.mockReturnValue(ClientType.Browser);
|
||||
deviceTrustService.getShouldTrustDevice.mockResolvedValue(true);
|
||||
i18nService.t.mockImplementation((key: string) => key);
|
||||
|
||||
component = new LoginDecryptionOptionsComponent(
|
||||
accountService,
|
||||
anonLayoutWrapperDataService,
|
||||
apiService,
|
||||
destroyRef,
|
||||
deviceTrustService,
|
||||
dialogService,
|
||||
formBuilder,
|
||||
i18nService,
|
||||
keyService,
|
||||
loginDecryptionOptionsService,
|
||||
loginEmailService,
|
||||
messagingService,
|
||||
organizationApiService,
|
||||
passwordResetEnrollmentService,
|
||||
platformUtilsService,
|
||||
router,
|
||||
ssoLoginService,
|
||||
toastService,
|
||||
userDecryptionOptionsService,
|
||||
validationService,
|
||||
logoutService,
|
||||
registerSdkService,
|
||||
securityStateService,
|
||||
appIdService,
|
||||
configService,
|
||||
accountCryptographicStateService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("createUser with feature flag enabled", () => {
|
||||
let mockPostKeysForTdeRegistration: jest.Mock;
|
||||
let mockRegistration: any;
|
||||
let mockAuth: any;
|
||||
let mockSdkValue: any;
|
||||
let mockSdkRef: any;
|
||||
let mockSdk: any;
|
||||
let mockDeviceKey: string;
|
||||
let mockDeviceKeyObj: SymmetricCryptoKey;
|
||||
let mockUserKeyBytes: Uint8Array;
|
||||
let mockPrivateKey: string;
|
||||
let mockSignedPublicKey: string;
|
||||
let mockSigningKey: string;
|
||||
let mockSecurityState: SignedSecurityState;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock asUuid to return the input value for test consistency
|
||||
jest.mock("@bitwarden/common/platform/abstractions/sdk/sdk.service", () => ({
|
||||
asUuid: (x: any) => x,
|
||||
}));
|
||||
(Symbol as any).dispose = Symbol("dispose");
|
||||
|
||||
mockPrivateKey = "mock-private-key";
|
||||
mockSignedPublicKey = "mock-signed-public-key";
|
||||
mockSigningKey = "mock-signing-key";
|
||||
mockSecurityState = {
|
||||
signature: "mock-signature",
|
||||
payload: {
|
||||
version: 2,
|
||||
timestamp: Date.now(),
|
||||
privateKeyHash: "mock-hash",
|
||||
},
|
||||
} as any;
|
||||
const deviceKeyBytes = new Uint8Array(32).fill(5);
|
||||
mockDeviceKey = Buffer.from(deviceKeyBytes).toString("base64");
|
||||
mockDeviceKeyObj = SymmetricCryptoKey.fromString(mockDeviceKey);
|
||||
mockUserKeyBytes = new Uint8Array(64);
|
||||
|
||||
mockPostKeysForTdeRegistration = jest.fn().mockResolvedValue({
|
||||
account_cryptographic_state: {
|
||||
V2: {
|
||||
private_key: mockPrivateKey,
|
||||
signed_public_key: mockSignedPublicKey,
|
||||
signing_key: mockSigningKey,
|
||||
security_state: mockSecurityState,
|
||||
},
|
||||
},
|
||||
device_key: mockDeviceKey,
|
||||
user_key: mockUserKeyBytes,
|
||||
});
|
||||
|
||||
mockRegistration = {
|
||||
post_keys_for_tde_registration: mockPostKeysForTdeRegistration,
|
||||
};
|
||||
|
||||
mockAuth = {
|
||||
registration: jest.fn().mockReturnValue(mockRegistration),
|
||||
};
|
||||
|
||||
mockSdkValue = {
|
||||
auth: jest.fn().mockReturnValue(mockAuth),
|
||||
};
|
||||
|
||||
mockSdkRef = {
|
||||
value: mockSdkValue,
|
||||
[Symbol.dispose]: jest.fn(),
|
||||
};
|
||||
|
||||
mockSdk = {
|
||||
take: jest.fn().mockReturnValue(mockSdkRef),
|
||||
};
|
||||
|
||||
registerSdkService.registerClient$ = jest.fn((userId: UserId) => of(mockSdk)) as any;
|
||||
|
||||
// Setup for new user state
|
||||
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
|
||||
of({
|
||||
trustedDeviceOption: {
|
||||
hasAdminApproval: false,
|
||||
hasLoginApprovingDevice: false,
|
||||
hasManageResetPasswordPermission: false,
|
||||
isTdeOffboarding: false,
|
||||
},
|
||||
hasMasterPassword: false,
|
||||
keyConnectorOption: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
ssoLoginService.getActiveUserOrganizationSsoIdentifier.mockResolvedValue("org-identifier");
|
||||
organizationApiService.getAutoEnrollStatus.mockResolvedValue({
|
||||
id: mockOrgId,
|
||||
resetPasswordEnabled: true,
|
||||
} as any);
|
||||
|
||||
// Initialize component to set up new user state
|
||||
await component.ngOnInit();
|
||||
});
|
||||
|
||||
it("should use SDK v2 registration when feature flag is enabled", async () => {
|
||||
// Arrange
|
||||
configService.getFeatureFlag.mockResolvedValue(true);
|
||||
loginDecryptionOptionsService.handleCreateUserSuccess.mockResolvedValue(undefined);
|
||||
router.navigate.mockResolvedValue(true);
|
||||
appIdService.getAppId.mockResolvedValue("mock-app-id");
|
||||
organizationApiService.getKeys.mockResolvedValue({
|
||||
publicKey: "mock-org-public-key",
|
||||
privateKey: "mock-org-private-key",
|
||||
} as any);
|
||||
|
||||
// Act
|
||||
await component["createUser"]();
|
||||
|
||||
// Assert
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
expect(appIdService.getAppId).toHaveBeenCalled();
|
||||
expect(organizationApiService.getKeys).toHaveBeenCalledWith(mockOrgId);
|
||||
expect(registerSdkService.registerClient$).toHaveBeenCalledWith(mockUserId);
|
||||
|
||||
// Verify SDK registration was called with correct parameters
|
||||
expect(mockSdkValue.auth).toHaveBeenCalled();
|
||||
expect(mockAuth.registration).toHaveBeenCalled();
|
||||
expect(mockPostKeysForTdeRegistration).toHaveBeenCalledWith({
|
||||
org_id: mockOrgId,
|
||||
org_public_key: "mock-org-public-key",
|
||||
user_id: mockUserId,
|
||||
device_identifier: "mock-app-id",
|
||||
trust_device: true,
|
||||
});
|
||||
|
||||
const expectedDeviceKey = mockDeviceKeyObj;
|
||||
const expectedUserKey = new SymmetricCryptoKey(new Uint8Array(mockUserKeyBytes));
|
||||
|
||||
// Verify keys were set
|
||||
expect(keyService.setPrivateKey).toHaveBeenCalledWith(mockPrivateKey, mockUserId);
|
||||
expect(keyService.setSignedPublicKey).toHaveBeenCalledWith(mockSignedPublicKey, mockUserId);
|
||||
expect(keyService.setUserSigningKey).toHaveBeenCalledWith(mockSigningKey, mockUserId);
|
||||
expect(securityStateService.setAccountSecurityState).toHaveBeenCalledWith(
|
||||
mockSecurityState,
|
||||
mockUserId,
|
||||
);
|
||||
expect(accountCryptographicStateService.setAccountCryptographicState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
V2: {
|
||||
private_key: mockPrivateKey,
|
||||
signed_public_key: mockSignedPublicKey,
|
||||
signing_key: mockSigningKey,
|
||||
security_state: mockSecurityState,
|
||||
},
|
||||
}),
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
expect(validationService.showError).not.toHaveBeenCalled();
|
||||
|
||||
// Verify device and user keys were persisted
|
||||
expect(deviceTrustService.setDeviceKey).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
expect.any(SymmetricCryptoKey),
|
||||
);
|
||||
expect(keyService.setUserKey).toHaveBeenCalledWith(
|
||||
expect.any(SymmetricCryptoKey),
|
||||
mockUserId,
|
||||
);
|
||||
|
||||
const [, deviceKeyArg] = deviceTrustService.setDeviceKey.mock.calls[0];
|
||||
const [userKeyArg] = keyService.setUserKey.mock.calls[0];
|
||||
|
||||
expect((deviceKeyArg as SymmetricCryptoKey).keyB64).toBe(expectedDeviceKey.keyB64);
|
||||
expect((userKeyArg as SymmetricCryptoKey).keyB64).toBe(expectedUserKey.keyB64);
|
||||
|
||||
// Verify success toast and navigation
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "accountSuccessfullyCreated",
|
||||
});
|
||||
expect(loginDecryptionOptionsService.handleCreateUserSuccess).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
|
||||
it("should use legacy registration when feature flag is disabled", async () => {
|
||||
// Arrange
|
||||
configService.getFeatureFlag.mockResolvedValue(false);
|
||||
|
||||
const mockPublicKey = "mock-public-key";
|
||||
const mockPrivateKey = {
|
||||
encryptedString: "mock-encrypted-private-key",
|
||||
} as any;
|
||||
|
||||
keyService.initAccount.mockResolvedValue({
|
||||
publicKey: mockPublicKey,
|
||||
privateKey: mockPrivateKey,
|
||||
} as any);
|
||||
|
||||
apiService.postAccountKeys.mockResolvedValue(undefined);
|
||||
passwordResetEnrollmentService.enroll.mockResolvedValue(undefined);
|
||||
deviceTrustService.trustDevice.mockResolvedValue(undefined);
|
||||
loginDecryptionOptionsService.handleCreateUserSuccess.mockResolvedValue(undefined);
|
||||
router.navigate.mockResolvedValue(true);
|
||||
|
||||
// Act
|
||||
await component["createUser"]();
|
||||
|
||||
// Assert
|
||||
expect(configService.getFeatureFlag).toHaveBeenCalledWith(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
expect(keyService.initAccount).toHaveBeenCalledWith(mockUserId);
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
publicKey: mockPublicKey,
|
||||
encryptedPrivateKey: mockPrivateKey.encryptedString,
|
||||
}),
|
||||
);
|
||||
expect(passwordResetEnrollmentService.enroll).toHaveBeenCalledWith(mockOrgId);
|
||||
expect(deviceTrustService.trustDevice).toHaveBeenCalledWith(mockUserId);
|
||||
|
||||
// Verify success toast
|
||||
expect(toastService.showToast).toHaveBeenCalledWith({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: "accountSuccessfullyCreated",
|
||||
});
|
||||
|
||||
// Verify navigation
|
||||
expect(loginDecryptionOptionsService.handleCreateUserSuccess).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(["/tabs/vault"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,17 @@ import { Component, DestroyRef, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, FormControl, ReactiveFormsModule } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
import { catchError, defer, firstValueFrom, from, map, of, switchMap, throwError } from "rxjs";
|
||||
import {
|
||||
catchError,
|
||||
concatMap,
|
||||
defer,
|
||||
firstValueFrom,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
throwError,
|
||||
} from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import {
|
||||
@@ -20,13 +30,27 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
|
||||
import { PasswordResetEnrollmentServiceAbstraction } from "@bitwarden/common/auth/abstractions/password-reset-enrollment.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AccountCryptographicStateService } from "@bitwarden/common/key-management/account-cryptography/account-cryptographic-state.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/key-management/device-trust/abstractions/device-trust.service.abstraction";
|
||||
import { SecurityStateService } from "@bitwarden/common/key-management/security-state/abstractions/security-state.service";
|
||||
import {
|
||||
SignedPublicKey,
|
||||
SignedSecurityState,
|
||||
WrappedSigningKey,
|
||||
} from "@bitwarden/common/key-management/types";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { RegisterSdkService } from "@bitwarden/common/platform/abstractions/sdk/register-sdk.service";
|
||||
import { asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DeviceKey, UserKey } from "@bitwarden/common/types/key";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
@@ -40,6 +64,7 @@ import {
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { OrganizationId as SdkOrganizationId, UserId as SdkUserId } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { LoginDecryptionOptionsService } from "./login-decryption-options.service";
|
||||
|
||||
@@ -112,6 +137,11 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private validationService: ValidationService,
|
||||
private logoutService: LogoutService,
|
||||
private registerSdkService: RegisterSdkService,
|
||||
private securityStateService: SecurityStateService,
|
||||
private appIdService: AppIdService,
|
||||
private configService: ConfigService,
|
||||
private accountCryptographicStateService: AccountCryptographicStateService,
|
||||
) {
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
@@ -251,9 +281,85 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId);
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
const useSdkV2Creation = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27279_V2RegistrationTdeJit,
|
||||
);
|
||||
if (useSdkV2Creation) {
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
const userId = this.activeAccountId;
|
||||
const organizationId = this.newUserOrgId;
|
||||
|
||||
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
|
||||
const register_result = await firstValueFrom(
|
||||
this.registerSdkService.registerClient$(userId).pipe(
|
||||
concatMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
|
||||
using ref = sdk.take();
|
||||
return await ref.value
|
||||
.auth()
|
||||
.registration()
|
||||
.post_keys_for_tde_registration({
|
||||
org_id: asUuid<SdkOrganizationId>(organizationId),
|
||||
org_public_key: orgKeyResponse.publicKey,
|
||||
user_id: asUuid<SdkUserId>(userId),
|
||||
device_identifier: deviceIdentifier,
|
||||
trust_device: this.formGroup.value.rememberDevice,
|
||||
});
|
||||
}),
|
||||
),
|
||||
);
|
||||
// The keys returned here can only be v2 keys, since the SDK only implements returning V2 keys.
|
||||
if ("V1" in register_result.account_cryptographic_state) {
|
||||
throw new Error("Unexpected V1 account cryptographic state");
|
||||
}
|
||||
|
||||
// Note: When SDK state management matures, these should be moved into post_keys_for_tde_registration
|
||||
// Set account cryptography state
|
||||
await this.accountCryptographicStateService.setAccountCryptographicState(
|
||||
register_result.account_cryptographic_state,
|
||||
userId,
|
||||
);
|
||||
// Legacy individual states
|
||||
await this.keyService.setPrivateKey(
|
||||
register_result.account_cryptographic_state.V2.private_key,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setSignedPublicKey(
|
||||
register_result.account_cryptographic_state.V2.signed_public_key as SignedPublicKey,
|
||||
userId,
|
||||
);
|
||||
await this.keyService.setUserSigningKey(
|
||||
register_result.account_cryptographic_state.V2.signing_key as WrappedSigningKey,
|
||||
userId,
|
||||
);
|
||||
await this.securityStateService.setAccountSecurityState(
|
||||
register_result.account_cryptographic_state.V2.security_state as SignedSecurityState,
|
||||
userId,
|
||||
);
|
||||
|
||||
// TDE unlock
|
||||
await this.deviceTrustService.setDeviceKey(
|
||||
userId,
|
||||
SymmetricCryptoKey.fromString(register_result.device_key) as DeviceKey,
|
||||
);
|
||||
|
||||
// Set user key - user is now unlocked
|
||||
await this.keyService.setUserKey(
|
||||
SymmetricCryptoKey.fromString(register_result.user_key) as UserKey,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
const { publicKey, privateKey } = await this.keyService.initAccount(this.activeAccountId);
|
||||
const keysRequest = new KeysRequest(publicKey, privateKey.encryptedString);
|
||||
await this.apiService.postAccountKeys(keysRequest);
|
||||
await this.passwordResetEnrollmentService.enroll(this.newUserOrgId);
|
||||
if (this.formGroup.value.rememberDevice) {
|
||||
await this.deviceTrustService.trustDevice(this.activeAccountId);
|
||||
}
|
||||
}
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -261,12 +367,6 @@ export class LoginDecryptionOptionsComponent implements OnInit {
|
||||
message: this.i18nService.t("accountSuccessfullyCreated"),
|
||||
});
|
||||
|
||||
await this.passwordResetEnrollmentService.enroll(this.newUserOrgId);
|
||||
|
||||
if (this.formGroup.value.rememberDevice) {
|
||||
await this.deviceTrustService.trustDevice(this.activeAccountId);
|
||||
}
|
||||
|
||||
await this.loginDecryptionOptionsService.handleCreateUserSuccess();
|
||||
|
||||
if (this.clientType === ClientType.Desktop) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function mockAccountInfoWith(info: Partial<AccountInfo> = {}): AccountInf
|
||||
name: "name",
|
||||
email: "email",
|
||||
emailVerified: true,
|
||||
creationDate: "2024-01-01T00:00:00.000Z",
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
...info,
|
||||
};
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export class FakeAccountService implements AccountService {
|
||||
await this.mock.setAccountEmailVerified(userId, emailVerified);
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
async setAccountCreationDate(userId: UserId, creationDate: Date): Promise<void> {
|
||||
await this.mock.setAccountCreationDate(userId, creationDate);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,33 +2,25 @@ import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
|
||||
/**
|
||||
* Holds state that represents a user's account with Bitwarden.
|
||||
* Any additions here should be added to the equality check in the AccountService
|
||||
* to ensure that emissions are done on every change.
|
||||
*
|
||||
* @property email - User's email address.
|
||||
* @property emailVerified - Whether the email has been verified.
|
||||
* @property name - User's display name (optional).
|
||||
* @property creationDate - Date when the account was created.
|
||||
* Will be undefined immediately after login until the first sync completes.
|
||||
*/
|
||||
export type AccountInfo = {
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
name: string | undefined;
|
||||
creationDate: string | undefined;
|
||||
creationDate: Date | undefined;
|
||||
};
|
||||
|
||||
export type Account = { id: UserId } & AccountInfo;
|
||||
|
||||
export function accountInfoEqual(a: AccountInfo, b: AccountInfo) {
|
||||
if (a == null && b == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = new Set([...Object.keys(a), ...Object.keys(b)]) as Set<keyof AccountInfo>;
|
||||
for (const key of keys) {
|
||||
if (a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export abstract class AccountService {
|
||||
abstract accounts$: Observable<Record<UserId, AccountInfo>>;
|
||||
|
||||
@@ -77,7 +69,7 @@ export abstract class AccountService {
|
||||
* @param userId
|
||||
* @param creationDate
|
||||
*/
|
||||
abstract setAccountCreationDate(userId: UserId, creationDate: string): Promise<void>;
|
||||
abstract setAccountCreationDate(userId: UserId, creationDate: Date): Promise<void>;
|
||||
/**
|
||||
* updates the `accounts$` observable with the new VerifyNewDeviceLogin property for the account.
|
||||
* @param userId
|
||||
|
||||
@@ -19,9 +19,21 @@ export class IdentityTokenResponse extends BaseResponse {
|
||||
tokenType: string;
|
||||
|
||||
// Decryption Information
|
||||
privateKey: string; // userKeyEncryptedPrivateKey
|
||||
|
||||
/**
|
||||
* privateKey is actually userKeyEncryptedPrivateKey
|
||||
* @deprecated Use {@link accountKeysResponseModel} instead
|
||||
*/
|
||||
privateKey: string;
|
||||
|
||||
// TODO: https://bitwarden.atlassian.net/browse/PM-30124 - Rename to just accountKeys
|
||||
accountKeysResponseModel: PrivateKeysResponseModel | null = null;
|
||||
key?: EncString; // masterKeyEncryptedUserKey
|
||||
|
||||
/**
|
||||
* key is actually masterKeyEncryptedUserKey
|
||||
* @deprecated Use {@link userDecryptionOptions.masterPasswordUnlock.masterKeyWrappedUserKey} instead
|
||||
*/
|
||||
key?: EncString;
|
||||
twoFactorToken: string;
|
||||
kdfConfig: KdfConfig;
|
||||
forcePasswordReset: boolean;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { AccountInfo, accountInfoEqual } from "../abstractions/account.service";
|
||||
import { AccountInfo } from "../abstractions/account.service";
|
||||
|
||||
import {
|
||||
ACCOUNT_ACCOUNTS,
|
||||
@@ -27,63 +27,6 @@ import {
|
||||
AccountServiceImplementation,
|
||||
} from "./account.service";
|
||||
|
||||
describe("accountInfoEqual", () => {
|
||||
const accountInfo = mockAccountInfoWith();
|
||||
|
||||
it("compares nulls", () => {
|
||||
expect(accountInfoEqual(null, null)).toBe(true);
|
||||
expect(accountInfoEqual(null, accountInfo)).toBe(false);
|
||||
expect(accountInfoEqual(accountInfo, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares all keys, not just those defined in AccountInfo", () => {
|
||||
const different = { ...accountInfo, extra: "extra" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares name", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, name: "name2" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares email", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, email: "email2" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares emailVerified", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, emailVerified: false };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares creationDate", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, creationDate: "2024-12-31T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares undefined creationDate", () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
|
||||
const same = { ...accountWithoutCreationDate };
|
||||
const different = { ...accountWithoutCreationDate, creationDate: "2024-01-01T00:00:00.000Z" };
|
||||
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
|
||||
expect(accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("accountService", () => {
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
@@ -121,6 +64,60 @@ describe("accountService", () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("accountInfoEqual", () => {
|
||||
const accountInfo = mockAccountInfoWith();
|
||||
|
||||
it("compares nulls", () => {
|
||||
expect((sut as any).accountInfoEqual(null, null)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(null, accountInfo)).toBe(false);
|
||||
expect((sut as any).accountInfoEqual(accountInfo, null)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares name", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, name: "name2" };
|
||||
|
||||
expect((sut as any).accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares email", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, email: "email2" };
|
||||
|
||||
expect((sut as any).accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares emailVerified", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, emailVerified: false };
|
||||
|
||||
expect((sut as any).accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares creationDate", () => {
|
||||
const same = { ...accountInfo };
|
||||
const different = { ...accountInfo, creationDate: new Date("2024-12-31T00:00:00.000Z") };
|
||||
|
||||
expect((sut as any).accountInfoEqual(accountInfo, same)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(accountInfo, different)).toBe(false);
|
||||
});
|
||||
|
||||
it("compares undefined creationDate", () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({ creationDate: undefined });
|
||||
const same = { ...accountWithoutCreationDate };
|
||||
const different = {
|
||||
...accountWithoutCreationDate,
|
||||
creationDate: new Date("2024-01-01T00:00:00.000Z"),
|
||||
};
|
||||
|
||||
expect((sut as any).accountInfoEqual(accountWithoutCreationDate, same)).toBe(true);
|
||||
expect((sut as any).accountInfoEqual(accountWithoutCreationDate, different)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("activeAccount$", () => {
|
||||
it("should emit null if no account is active", () => {
|
||||
const emissions = trackEmissions(sut.activeAccount$);
|
||||
@@ -281,7 +278,7 @@ describe("accountService", () => {
|
||||
});
|
||||
|
||||
it("should update the account with a new creation date", async () => {
|
||||
const newCreationDate = "2024-12-31T00:00:00.000Z";
|
||||
const newCreationDate = new Date("2024-12-31T00:00:00.000Z");
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
@@ -297,6 +294,24 @@ describe("accountService", () => {
|
||||
expect(currentState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it("should not update if the creation date has the same timestamp but different Date object", async () => {
|
||||
const sameTimestamp = new Date(userInfo.creationDate.getTime());
|
||||
await sut.setAccountCreationDate(userId, sameTimestamp);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it("should update if the creation date has a different timestamp", async () => {
|
||||
const differentDate = new Date(userInfo.creationDate.getTime() + 1000);
|
||||
await sut.setAccountCreationDate(userId, differentDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: differentDate },
|
||||
});
|
||||
});
|
||||
|
||||
it("should update from undefined to a defined creation date", async () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({
|
||||
...userInfo,
|
||||
@@ -304,7 +319,7 @@ describe("accountService", () => {
|
||||
});
|
||||
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
|
||||
|
||||
const newCreationDate = "2024-06-15T12:30:00.000Z";
|
||||
const newCreationDate = new Date("2024-06-15T12:30:00.000Z");
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
@@ -313,14 +328,19 @@ describe("accountService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should update to a different creation date string format", async () => {
|
||||
const newCreationDate = "2023-03-15T08:45:30.123Z";
|
||||
await sut.setAccountCreationDate(userId, newCreationDate);
|
||||
const currentState = await firstValueFrom(accountsState.state$);
|
||||
|
||||
expect(currentState).toEqual({
|
||||
[userId]: { ...userInfo, creationDate: newCreationDate },
|
||||
it("should not update when both creation dates are undefined", async () => {
|
||||
const accountWithoutCreationDate = mockAccountInfoWith({
|
||||
...userInfo,
|
||||
creationDate: undefined,
|
||||
});
|
||||
accountsState.stateSubject.next({ [userId]: accountWithoutCreationDate });
|
||||
|
||||
// Attempt to set to undefined (shouldn't trigger update)
|
||||
const currentStateBefore = await firstValueFrom(accountsState.state$);
|
||||
|
||||
// We can't directly call setAccountCreationDate with undefined, but we can verify
|
||||
// the behavior through setAccountInfo which accountInfoEqual uses internally
|
||||
expect(currentStateBefore[userId].creationDate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Account,
|
||||
AccountInfo,
|
||||
InternalAccountService,
|
||||
accountInfoEqual,
|
||||
} from "../../auth/abstractions/account.service";
|
||||
import { LogService } from "../../platform/abstractions/log.service";
|
||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||
@@ -37,7 +36,10 @@ export const ACCOUNT_ACCOUNTS = KeyDefinition.record<AccountInfo, UserId>(
|
||||
ACCOUNT_DISK,
|
||||
"accounts",
|
||||
{
|
||||
deserializer: (accountInfo) => accountInfo,
|
||||
deserializer: (accountInfo) => ({
|
||||
...accountInfo,
|
||||
creationDate: accountInfo.creationDate ? new Date(accountInfo.creationDate) : undefined,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -111,7 +113,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
this.activeAccount$ = this.activeAccountIdState.state$.pipe(
|
||||
combineLatestWith(this.accounts$),
|
||||
map(([id, accounts]) => (id ? ({ id, ...(accounts[id] as AccountInfo) } as Account) : null)),
|
||||
distinctUntilChanged((a, b) => a?.id === b?.id && accountInfoEqual(a, b)),
|
||||
distinctUntilChanged((a, b) => a?.id === b?.id && this.accountInfoEqual(a, b)),
|
||||
shareReplay({ bufferSize: 1, refCount: false }),
|
||||
);
|
||||
this.accountActivity$ = this.globalStateProvider
|
||||
@@ -168,7 +170,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
await this.setAccountInfo(userId, { emailVerified });
|
||||
}
|
||||
|
||||
async setAccountCreationDate(userId: UserId, creationDate: string): Promise<void> {
|
||||
async setAccountCreationDate(userId: UserId, creationDate: Date): Promise<void> {
|
||||
await this.setAccountInfo(userId, { creationDate });
|
||||
}
|
||||
|
||||
@@ -274,6 +276,23 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
this._showHeader$.next(visible);
|
||||
}
|
||||
|
||||
private accountInfoEqual(a: AccountInfo, b: AccountInfo) {
|
||||
if (a == null && b == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (a == null || b == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
a.email === b.email &&
|
||||
a.emailVerified === b.emailVerified &&
|
||||
a.name === b.name &&
|
||||
a.creationDate?.getTime() === b.creationDate?.getTime()
|
||||
);
|
||||
}
|
||||
|
||||
private async setAccountInfo(userId: UserId, update: Partial<AccountInfo>): Promise<void> {
|
||||
function newAccountInfo(oldAccountInfo: AccountInfo): AccountInfo {
|
||||
return { ...oldAccountInfo, ...update };
|
||||
@@ -291,7 +310,7 @@ export class AccountServiceImplementation implements InternalAccountService {
|
||||
throw new Error("Account does not exist");
|
||||
}
|
||||
|
||||
return !accountInfoEqual(accounts[userId], newAccountInfo(accounts[userId]));
|
||||
return !this.accountInfoEqual(accounts[userId], newAccountInfo(accounts[userId]));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -44,6 +44,7 @@ export enum FeatureFlag {
|
||||
NoLogoutOnKdfChange = "pm-23995-no-logout-on-kdf-change",
|
||||
DataRecoveryTool = "pm-28813-data-recovery-tool",
|
||||
ConsolidatedSessionTimeoutComponent = "pm-26056-consolidated-session-timeout-component",
|
||||
PM27279_V2RegistrationTdeJit = "pm-27279-v2-registration-tde-jit",
|
||||
|
||||
/* Tools */
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
@@ -62,7 +63,6 @@ export enum FeatureFlag {
|
||||
PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view",
|
||||
PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
AutofillConfirmation = "pm-25083-autofill-confirm-from-search",
|
||||
RiskInsightsForPremium = "pm-23904-risk-insights-for-premium",
|
||||
VaultLoadingSkeletons = "pm-25081-vault-skeleton-loaders",
|
||||
BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight",
|
||||
@@ -70,8 +70,6 @@ export enum FeatureFlag {
|
||||
|
||||
/* Platform */
|
||||
IpcChannelFramework = "ipc-channel-framework",
|
||||
InactiveUserServerNotification = "pm-25130-receive-push-notifications-for-inactive-users",
|
||||
PushNotificationsWhenLocked = "pm-19388-push-notifications-when-locked",
|
||||
|
||||
/* Innovation */
|
||||
PM19148_InnovationArchive = "pm-19148-innovation-archive",
|
||||
@@ -125,7 +123,6 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM19941MigrateCipherDomainToSdk]: FALSE,
|
||||
[FeatureFlag.PM22134SdkCipherListView]: FALSE,
|
||||
[FeatureFlag.PM22136_SdkCipherEncryption]: FALSE,
|
||||
[FeatureFlag.AutofillConfirmation]: FALSE,
|
||||
[FeatureFlag.RiskInsightsForPremium]: FALSE,
|
||||
[FeatureFlag.VaultLoadingSkeletons]: FALSE,
|
||||
[FeatureFlag.BrowserPremiumSpotlight]: FALSE,
|
||||
@@ -154,11 +151,10 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.NoLogoutOnKdfChange]: FALSE,
|
||||
[FeatureFlag.DataRecoveryTool]: FALSE,
|
||||
[FeatureFlag.ConsolidatedSessionTimeoutComponent]: FALSE,
|
||||
[FeatureFlag.PM27279_V2RegistrationTdeJit]: FALSE,
|
||||
|
||||
/* Platform */
|
||||
[FeatureFlag.IpcChannelFramework]: FALSE,
|
||||
[FeatureFlag.InactiveUserServerNotification]: FALSE,
|
||||
[FeatureFlag.PushNotificationsWhenLocked]: FALSE,
|
||||
|
||||
/* Innovation */
|
||||
[FeatureFlag.PM19148_InnovationArchive]: FALSE,
|
||||
|
||||
@@ -39,6 +39,7 @@ export abstract class DeviceTrustServiceAbstraction {
|
||||
|
||||
/** Retrieves the device key if it exists from state or secure storage if supported for the active user. */
|
||||
abstract getDeviceKey(userId: UserId): Promise<DeviceKey | null>;
|
||||
abstract setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void>;
|
||||
abstract decryptUserKeyWithDeviceKey(
|
||||
userId: UserId,
|
||||
encryptedDevicePrivateKey: EncString,
|
||||
|
||||
@@ -356,7 +356,7 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
|
||||
}
|
||||
}
|
||||
|
||||
private async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> {
|
||||
async setDeviceKey(userId: UserId, deviceKey: DeviceKey | null): Promise<void> {
|
||||
if (!userId) {
|
||||
throw new Error("UserId is required. Cannot set device key.");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { BehaviorSubject, bufferCount, firstValueFrom, Subject, ObservedValueOf
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AuthRequestAnsweringService } from "@bitwarden/common/auth/abstractions/auth-request-answering/auth-request-answering.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import { mockAccountInfoWith } from "../../../../spec";
|
||||
import { AccountService } from "../../../auth/abstractions/account.service";
|
||||
@@ -130,15 +129,6 @@ describe("DefaultServerNotificationsService (multi-user)", () => {
|
||||
|
||||
authRequestAnsweringService = mock<AuthRequestAnsweringService>();
|
||||
|
||||
configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => {
|
||||
const flagValueByFlag: Partial<Record<FeatureFlag, boolean>> = {
|
||||
[FeatureFlag.InactiveUserServerNotification]: true,
|
||||
[FeatureFlag.PushNotificationsWhenLocked]: true,
|
||||
};
|
||||
return new BehaviorSubject(flagValueByFlag[flag] ?? false) as any;
|
||||
});
|
||||
|
||||
policyService = mock<InternalPolicyService>();
|
||||
|
||||
defaultServerNotificationsService = new DefaultServerNotificationsService(
|
||||
|
||||
@@ -71,48 +71,20 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
private readonly configService: ConfigService,
|
||||
private readonly policyService: InternalPolicyService,
|
||||
) {
|
||||
this.notifications$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification)
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((inactiveUserServerNotificationEnabled) => {
|
||||
if (inactiveUserServerNotificationEnabled) {
|
||||
return this.accountService.accounts$.pipe(
|
||||
map((accounts: Record<UserId, AccountInfo>): Set<UserId> => {
|
||||
const validUserIds = Object.entries(accounts)
|
||||
.filter(
|
||||
([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified,
|
||||
)
|
||||
.map(([userId, _]) => userId as UserId);
|
||||
return new Set(validUserIds);
|
||||
}),
|
||||
trackedMerge((id: UserId) => {
|
||||
return this.userNotifications$(id as UserId).pipe(
|
||||
map(
|
||||
(notification: NotificationResponse) => [notification, id as UserId] as const,
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
distinctUntilChanged(),
|
||||
switchMap((activeAccountId) => {
|
||||
if (activeAccountId == null) {
|
||||
// We don't emit server-notifications for inactive accounts currently
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return this.userNotifications$(activeAccountId).pipe(
|
||||
map((notification) => [notification, activeAccountId] as const),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
share(), // Multiple subscribers should only create a single connection to the server
|
||||
);
|
||||
this.notifications$ = this.accountService.accounts$.pipe(
|
||||
map((accounts: Record<UserId, AccountInfo>): Set<UserId> => {
|
||||
const validUserIds = Object.entries(accounts)
|
||||
.filter(([_, accountInfo]) => accountInfo.email !== "" || accountInfo.emailVerified)
|
||||
.map(([userId, _]) => userId as UserId);
|
||||
return new Set(validUserIds);
|
||||
}),
|
||||
trackedMerge((id: UserId) => {
|
||||
return this.userNotifications$(id as UserId).pipe(
|
||||
map((notification: NotificationResponse) => [notification, id as UserId] as const),
|
||||
);
|
||||
}),
|
||||
share(), // Multiple subscribers should only create a single connection to the server
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,25 +147,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
}
|
||||
|
||||
private hasAccessToken$(userId: UserId) {
|
||||
return this.configService.getFeatureFlag$(FeatureFlag.PushNotificationsWhenLocked).pipe(
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map(
|
||||
(authStatus) =>
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.Unlocked,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
switchMap((featureFlagEnabled) => {
|
||||
if (featureFlagEnabled) {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map(
|
||||
(authStatus) =>
|
||||
authStatus === AuthenticationStatus.Locked ||
|
||||
authStatus === AuthenticationStatus.Unlocked,
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
} else {
|
||||
return this.authService.authStatusFor$(userId).pipe(
|
||||
map((authStatus) => authStatus === AuthenticationStatus.Unlocked),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -208,19 +168,13 @@ export class DefaultServerNotificationsService implements ServerNotificationsSer
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.InactiveUserServerNotification),
|
||||
)
|
||||
) {
|
||||
const activeAccountId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const activeAccountId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
const isActiveUser = activeAccountId === userId;
|
||||
if (!isActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
|
||||
return;
|
||||
}
|
||||
const notificationIsForActiveUser = activeAccountId === userId;
|
||||
if (!notificationIsForActiveUser && !AllowedMultiUserNotificationTypes.has(notification.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (notification.type) {
|
||||
|
||||
@@ -279,8 +279,8 @@ export class DefaultSyncService extends CoreSyncService {
|
||||
await this.avatarService.setSyncAvatarColor(response.id, response.avatarColor);
|
||||
await this.tokenService.setSecurityStamp(response.securityStamp, response.id);
|
||||
await this.accountService.setAccountEmailVerified(response.id, response.emailVerified);
|
||||
await this.accountService.setAccountCreationDate(response.id, new Date(response.creationDate));
|
||||
await this.accountService.setAccountVerifyNewDeviceLogin(response.id, response.verifyDevices);
|
||||
await this.accountService.setAccountCreationDate(response.id, response.creationDate);
|
||||
|
||||
await this.billingAccountProfileStateService.setHasPremium(
|
||||
response.premiumPersonally,
|
||||
|
||||
@@ -207,9 +207,13 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* Update the local store of CipherData with the provided data. Values are upserted into the existing store.
|
||||
*
|
||||
* @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects.
|
||||
* @param userId Optional user ID for whom the cipher data is being upserted.
|
||||
* @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated
|
||||
*/
|
||||
abstract upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>>;
|
||||
abstract upsert(
|
||||
cipher: CipherData | CipherData[],
|
||||
userId?: UserId,
|
||||
): Promise<Record<CipherId, CipherData>>;
|
||||
abstract replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any>;
|
||||
abstract clear(userId?: string): Promise<void>;
|
||||
abstract moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise<any>;
|
||||
|
||||
@@ -1196,12 +1196,15 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.encryptedCiphersState(userId).update(() => ciphers);
|
||||
}
|
||||
|
||||
async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> {
|
||||
async upsert(
|
||||
cipher: CipherData | CipherData[],
|
||||
userId?: UserId,
|
||||
): Promise<Record<CipherId, CipherData>> {
|
||||
const ciphers = cipher instanceof CipherData ? [cipher] : cipher;
|
||||
const res = await this.updateEncryptedCipherState((current) => {
|
||||
ciphers.forEach((c) => (current[c.id as CipherId] = c));
|
||||
return current;
|
||||
});
|
||||
}, userId);
|
||||
// Some state storage providers (e.g. Electron) don't update the state immediately, wait for next tick
|
||||
// Otherwise, subscribers to cipherViews$ can get stale data
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
@@ -219,7 +219,7 @@ describe("DefaultCipherArchiveService", () => {
|
||||
} as any,
|
||||
}),
|
||||
);
|
||||
mockCipherService.replace.mockResolvedValue(undefined);
|
||||
mockCipherService.upsert.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should archive single cipher", async () => {
|
||||
@@ -233,13 +233,13 @@ describe("DefaultCipherArchiveService", () => {
|
||||
true,
|
||||
);
|
||||
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[cipherId]: expect.objectContaining({
|
||||
expect(mockCipherService.upsert).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
archivedDate: "2024-01-15T10:30:00.000Z",
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
userId,
|
||||
);
|
||||
});
|
||||
@@ -282,7 +282,7 @@ describe("DefaultCipherArchiveService", () => {
|
||||
} as any,
|
||||
}),
|
||||
);
|
||||
mockCipherService.replace.mockResolvedValue(undefined);
|
||||
mockCipherService.upsert.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it("should unarchive single cipher", async () => {
|
||||
@@ -296,12 +296,12 @@ describe("DefaultCipherArchiveService", () => {
|
||||
true,
|
||||
);
|
||||
expect(mockCipherService.ciphers$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCipherService.replace).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
[cipherId]: expect.objectContaining({
|
||||
expect(mockCipherService.upsert).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
revisionDate: "2024-01-15T10:31:00.000Z",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.replace(currentCiphers, userId);
|
||||
await this.cipherService.upsert(Object.values(currentCiphers), userId);
|
||||
}
|
||||
|
||||
async unarchiveWithServer(ids: CipherId | CipherId[], userId: UserId): Promise<void> {
|
||||
@@ -116,6 +116,6 @@ export class DefaultCipherArchiveService implements CipherArchiveService {
|
||||
localCipher.revisionDate = cipher.revisionDate;
|
||||
}
|
||||
|
||||
await this.cipherService.replace(currentCiphers, userId);
|
||||
await this.cipherService.upsert(Object.values(currentCiphers), userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inject, Injectable } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
import { skip, filter, map, combineLatestWith, tap } from "rxjs";
|
||||
import { skip, filter, combineLatestWith, tap } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
@@ -19,8 +19,10 @@ export class RouterFocusManagerService {
|
||||
*
|
||||
* By default, we focus the `main` after an internal route navigation.
|
||||
*
|
||||
* Consumers can opt out of the passing the following to the `info` input:
|
||||
* `<a [routerLink]="route()" [info]="{ focusMainAfterNav: false }"></a>`
|
||||
* Consumers can opt out of the passing the following to the `state` input. Using `state`
|
||||
* allows us to access the value between browser back/forward arrows.
|
||||
* In template: `<a [routerLink]="route()" [state]="{ focusMainAfterNav: false }"></a>`
|
||||
* In typescript: `this.router.navigate([], { state: { focusMainAfterNav: false }})`
|
||||
*
|
||||
* Or, consumers can use the autofocus directive on an applicable interactive element.
|
||||
* The autofocus directive will take precedence over this route focus pipeline.
|
||||
@@ -44,15 +46,12 @@ export class RouterFocusManagerService {
|
||||
skip(1),
|
||||
combineLatestWith(this.configService.getFeatureFlag$(FeatureFlag.RouterFocusManagement)),
|
||||
filter(([_navEvent, flagEnabled]) => flagEnabled),
|
||||
map(() => {
|
||||
const currentNavData = this.router.getCurrentNavigation()?.extras;
|
||||
filter(() => {
|
||||
const currentNavExtras = this.router.currentNavigation()?.extras;
|
||||
|
||||
const info = currentNavData?.info as { focusMainAfterNav?: boolean } | undefined;
|
||||
const focusMainAfterNav: boolean | undefined = currentNavExtras?.state?.focusMainAfterNav;
|
||||
|
||||
return info;
|
||||
}),
|
||||
filter((currentNavInfo) => {
|
||||
return currentNavInfo === undefined ? true : currentNavInfo?.focusMainAfterNav !== false;
|
||||
return focusMainAfterNav !== false;
|
||||
}),
|
||||
tap(() => {
|
||||
const mainEl = document.querySelector<HTMLElement>("main");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user